Compare commits

..

184 Commits

Author SHA1 Message Date
Benyamin Hirschberg
1cfcc6d930 Merge pull request #90 from BenHirschbergCa/master
installing kubescape in home directory if possible
2021-09-23 21:05:31 +03:00
Ben Hirschberg
8c6f618743 installing kubescape in home directory if possible 2021-09-23 17:54:05 +00:00
Benyamin Hirschberg
6adf1c3162 Merge pull request #88 from BenHirschbergCa/master
Fixing sudo missing issue and returning build without C runtime dependency
2021-09-21 23:59:30 +03:00
Ben Hirschberg
9f5f4f1832 removing useless command in install 2021-09-21 23:48:26 +03:00
Ben Hirschberg
9f49cc83e9 Fixing install.sh to work in environments where there is no sudo && enabling static executable build 2021-09-21 23:45:35 +03:00
Benyamin Hirschberg
67972199ce Merge pull request #86 from armosec/Daniel-GrunbergerCA-patch-1
Update README.md
2021-09-21 23:05:25 +03:00
Daniel Grunberger
9d5db86bf3 Update README.md 2021-09-19 17:42:59 +03:00
Benyamin Hirschberg
39efed5fc1 Merge pull request #85 from armosec/dev
Delivering signup to master
2021-09-19 17:34:31 +03:00
Benyamin Hirschberg
21c2bf22dd Update scaninfo.go 2021-09-19 17:28:46 +03:00
Benyamin Hirschberg
6c94b3a423 Merge branch 'master' into dev 2021-09-19 17:24:14 +03:00
Benyamin Hirschberg
683248db0b Merge pull request #83 from Daniel-GrunbergerCA/master
Finish config local flags
2021-09-19 17:08:56 +03:00
Daniel Grunberger
9032400528 Fix permission denied 2021-09-19 16:19:25 +03:00
Bezbran
1a925b1acf Merge pull request #84 from Bezbran/dev
attributes as lower
2021-09-19 15:09:01 +03:00
Bezalel Brandwine
dadc8c2c60 exception attributes to lower 2021-09-19 15:07:39 +03:00
Daniel-GrunbergerCA
01c1b44bfc finish config local flags 2021-09-19 14:53:04 +03:00
Bezbran
a394a99d8f Merge pull request #2 from armosec/dev
Dev from armosec
2021-09-19 13:39:31 +03:00
Benyamin Hirschberg
4213707b7f Merge pull request #82 from brsolomon-deloitte/bugfix-issue-81-output-ext
Bugfix: correctly compare filepath.Ext() result
2021-09-18 21:26:39 +03:00
Brad Solomon
d53b8272ee Bugfix: correctly compare filepath.Ext() result
Closes #81.

setOutputFile() will incorrectly append .json to an --output
value that already has it. This is because
https://pkg.go.dev/path/filepath#Ext result includes
the ., whereas the current logic only tests against
json, not .json.
2021-09-16 11:57:58 -04:00
Daniel-GrunbergerCA
fdd688ac68 Merge remote-tracking branch 'upstream/dev' 2021-09-14 17:53:35 +03:00
Daniel-GrunbergerCA
5bb961bdc6 start get/set for config.json 2021-09-14 17:53:21 +03:00
Daniel-GrunbergerCA
9e7cc06f97 give higher priority to config.json 2021-09-14 17:14:21 +03:00
Benyamin Hirschberg
1d184d9000 Merge pull request #78 from YiscahLevySilas1/master
remove redundant responses
2021-09-14 16:52:05 +03:00
Benyamin Hirschberg
a5e2ebf647 Update datastructuresmethods.go 2021-09-14 16:51:16 +03:00
Benyamin Hirschberg
29eb573de5 Merge pull request #79 from BenHirschbergCa/dev
Add build for dev branch
2021-09-14 16:49:45 +03:00
Ben Hirschberg
ec6c3da5ec no needs :) 2021-09-14 16:40:12 +03:00
yiscah
e2d4f8961e Merge remote-tracking branch 'upstream/dev' 2021-09-14 16:38:13 +03:00
Ben Hirschberg
a48c680201 build dev branch 2021-09-14 16:36:57 +03:00
yiscah
c869f2c962 append to msg, loop backward on ruleresponses, don't check redundant role/clusterrole k8sresources 2021-09-14 16:36:57 +03:00
Benyamin Hirschberg
f77fc9a06d Merge branch 'armosec:dev' into dev 2021-09-14 16:01:59 +03:00
Daniel Grunberger
e12eae93b9 Merge pull request #77 from Daniel-GrunbergerCA/master
Add  config clsuter & fix github workflow
2021-09-14 15:27:09 +03:00
Daniel-GrunbergerCA
d92fb32574 fix build.yaml 2021-09-14 15:14:26 +03:00
Daniel-GrunbergerCA
541dba3d79 update build.yaml 2021-09-14 15:06:18 +03:00
yiscah
033ed17125 controlReport status is passed only if ALL ruleReports passed 2021-09-14 14:58:10 +03:00
Daniel-GrunbergerCA
aaeb663d15 remove env vars 2021-09-14 14:02:15 +03:00
Daniel-GrunbergerCA
c337005985 update build.yaml 2021-09-14 13:55:53 +03:00
Daniel-GrunbergerCA
192eeee348 update build.yaml for testing 2021-09-14 13:47:34 +03:00
yiscah
27c97684b9 delete redundant rule responses 2021-09-14 13:38:53 +03:00
Daniel-GrunbergerCA
41d5fa70ed Merge remote-tracking branch 'upstream/dev' 2021-09-14 13:21:25 +03:00
Bezalel Brandwine
4206e9c175 add caution for URLs changes 2021-09-14 12:52:35 +03:00
Daniel-GrunbergerCA
8658bb05dd change token to invitation in configmap 2021-09-14 12:23:28 +03:00
Daniel-GrunbergerCA
9b707016a9 Add set/get key-value option 2021-09-14 11:50:53 +03:00
Bezalel Brandwine
4b02826883 invitation token as param in configmap 2021-09-14 10:36:51 +03:00
Bezalel Brandwine
b29774ea71 some outputs refining 2021-09-14 09:58:17 +03:00
Benyamin Hirschberg
bf68e90a8e Merge pull request #8 from armosec/dev
Dev
2021-09-13 17:07:58 +03:00
Benyamin Hirschberg
cc5cdcd831 Merge branch 'dev' into dev 2021-09-13 17:07:51 +03:00
lalafi@cyberarmor.io
07f23ff7d9 Merge branch 'dev' of ssh://github.com/armosec/kubescape into dev 2021-09-13 16:33:32 +03:00
lalafi@cyberarmor.io
2985da6dc9 remove redundant field 2021-09-13 16:33:25 +03:00
Bezalel Brandwine
1523973749 change consts to var so ldflag -s will work 2021-09-13 15:26:29 +03:00
Bezalel Brandwine
ccafd78a14 change URLs to production at build time 2021-09-13 15:10:43 +03:00
Bezalel Brandwine
4f71fe0d55 support rerun on the same cluster flows 2021-09-13 14:25:09 +03:00
David Wertenteil
7bd6b6b4d1 Merge pull request #75 from dwertent/master
Handle download framework error
2021-09-13 11:31:25 +03:00
dwertent
2b976489a2 handle download framework error 2021-09-13 11:21:57 +03:00
dwertent
1440f20f95 merged from master 2021-09-13 11:01:49 +03:00
dwertent
941e7e27c0 Merge branch 'master' into dev 2021-09-13 11:00:30 +03:00
dwertent
5428c6ab2f merged from dev 2021-09-13 10:57:34 +03:00
David Wertenteil
851bb65d17 Merge pull request #74 from dwertent/master
* Update exception support
* Update installation script
2021-09-13 10:53:10 +03:00
dwertent
a3ce04b7e8 update install script 2021-09-13 10:47:29 +03:00
dwertent
4d68ca6aa2 update exceptions support 2021-09-13 10:24:24 +03:00
Ben Hirschberg
29c6767d3c add run locally to readme 2021-09-13 09:41:16 +03:00
Benyamin Hirschberg
d8f5f7975c Merge pull request #73 from BenHirschbergCa/dev
Inverting posture score
2021-09-13 08:34:22 +03:00
Ben Hirschberg
4cd8476837 Inverting posture score 2021-09-13 08:31:13 +03:00
Benyamin Hirschberg
112257449f Merge pull request #72 from BenHirschbergCa/dev
fixing API path
2021-09-13 08:23:22 +03:00
Ben Hirschberg
ff9cf4adf0 fixing API path 2021-09-13 08:22:25 +03:00
Benyamin Hirschberg
f2d387bc9c Merge pull request #71 from Daniel-GrunbergerCA/master
Support score, cluster name and results flag
2021-09-12 20:53:49 +03:00
lalafi@cyberarmor.io
2ceb5150e2 adding control id 2021-09-12 19:28:11 +03:00
Daniel-GrunbergerCA
00006ec721 rm omit empty 2021-09-12 18:49:13 +03:00
Daniel-GrunbergerCA
00aa6948ab Merge remote-tracking branch 'upstream/dev' 2021-09-12 18:47:31 +03:00
lalafi@cyberarmor.io
aad32ec965 added controlID 2021-09-12 18:43:45 +03:00
Daniel-GrunbergerCA
bd24ed3af7 add framework score 2021-09-12 18:37:50 +03:00
Daniel-GrunbergerCA
9fc455bcec Update posture report flag and cluster name 2021-09-12 18:13:24 +03:00
dwertent
0f8ba1e7e8 Merge branches 'master' and 'master' of github.com:armosec/kubescape 2021-09-12 17:45:32 +03:00
David Wertenteil
d3137af3d7 skip score updating 2021-09-12 17:36:23 +03:00
dwertent
775dd037d6 Merge branch 'dev' 2021-09-12 17:34:27 +03:00
dwertent
49cbfe130c do not calculate score 2021-09-12 17:34:00 +03:00
David Wertenteil
9cd61dd996 Merge pull request #70 from Moshe-Rappaport-CA/master
Support control IDs
2021-09-12 17:30:27 +03:00
moshep
292c4aa060 support id 2021-09-12 16:13:33 +03:00
moshep
56a265930d Merge remote-tracking branch 'upstream/dev' 2021-09-12 14:21:19 +03:00
Daniel-GrunbergerCA
4171d110a4 separa interfaces 2021-09-12 11:06:43 +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
Daniel-GrunbergerCA
d0775565e9 Merge remote-tracking branch 'upstream/dev' 2021-09-12 09:40:17 +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
David Wertenteil
ca4d4a096c Support self registration 2021-09-10 01:20:28 +03:00
dwertent
ff27db6b83 Merge remote-tracking branch 'upstream/dev' 2021-09-10 00:58:24 +03:00
Daniel Grunberger
c9ecb6c563 Add version handling (#67)
* start version handling

* add version handling

* update latest version check

* update build.yaml

* erase unused vars

* fix build.yaml

* fix var name

* handle error
2021-09-10 00:57:33 +03:00
dwertent
b9e5782264 support self registeration 2021-09-10 00:56:51 +03:00
Daniel-GrunbergerCA
d852f81cb0 handle error 2021-09-09 19:56:29 +03:00
dwertent
6137aa5d8e support fronegg 2021-09-09 17:15:55 +03:00
Daniel-GrunbergerCA
131b67ee83 fix var name 2021-09-09 16:26:42 +03:00
Daniel-GrunbergerCA
db6f00be08 fix build.yaml 2021-09-09 15:23:34 +03:00
Daniel-GrunbergerCA
2ecc80985a Merge remote-tracking branch 'upstream/dev' 2021-09-09 15:18:56 +03:00
Daniel-GrunbergerCA
93dbfd5110 erase unused vars 2021-09-09 15:15:36 +03:00
Daniel-GrunbergerCA
ae0c384c85 update build.yaml 2021-09-09 15:02:48 +03:00
Benyamin Hirschberg
f60ff1fb26 Merge pull request #63 from zc2638/feat/dockerfile
add docker build
2021-09-09 14:59:46 +03:00
Daniel-GrunbergerCA
08a81696a1 update latest version check 2021-09-09 14:58:52 +03:00
Daniel-GrunbergerCA
8375a8ae63 add version handling 2021-09-09 14:37:25 +03:00
Daniel-GrunbergerCA
31d8cf5118 start version handling 2021-09-09 12:29:42 +03:00
dwertent
597b967e55 Merge remote-tracking branch 'upstream/dev' 2021-09-09 11:43:44 +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
dwertent
3f84ee3fcc update summary 2021-09-05 14:42:49 +03:00
dwertent
38103ac90b update screenshot 2021-09-05 14:39:13 +03:00
dwertent
13d27697e1 update readme 2021-09-05 14:33:32 +03:00
dwertent
942f356d19 support exceptions 2021-09-05 14:21:51 +03:00
dwertent
b87b687e2f support exceptions 2021-09-02 17:41:03 +03:00
dwertent
2e313719bb adding scaore and excpetion to code 2021-09-02 14:54:04 +03:00
dwertent
0c5eb48fdb Merge remote-tracking branch 'upstream/master' 2021-09-02 13:21:13 +03:00
dwertent
2ae2c81e0b rm download 2021-09-02 13:18:23 +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
dwertent
222b154505 store file localy 2021-08-31 17:08:02 +03:00
dwertent
67c2de74f1 adding download script 2021-08-31 17:05:16 +03:00
dwertent
4a9b36807a remove sudo 2021-08-31 16:43:37 +03:00
dwertent
c6241fab38 remove sudo 2021-08-31 16:42:12 +03:00
dwertent
afbc69c6d2 Merge remote-tracking branch 'upstream/dev' 2021-08-31 16:41:44 +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
dwertent
2779cb4e25 update module 2021-08-31 11:47:44 +03:00
dwertent
f46ee93539 update modul name 2021-08-31 11:39:27 +03:00
dwertent
3eb087e5c1 Merge remote-tracking branch 'upstream/dev' 2021-08-31 11:38:14 +03:00
dwertent
59c935e723 update output f 2021-08-31 09:00:52 +03:00
dwertent
bae45d277f Merge remote-tracking branch 'upstream/dev' 2021-08-31 08:47:37 +03:00
dwertent
0b6dfa9cd0 Merge remote-tracking branch 'upstream/dev' 2021-08-30 18:47:07 +03:00
dwertent
1ff3a6c92c support output to file 2021-08-30 18:44:42 +03:00
dwertent
f75cee0d78 support stdin input 2021-08-30 14:54:01 +03:00
dwertent
229f16cb01 Merge remote-tracking branch 'upstream/dev' 2021-08-30 13:52:58 +03:00
dwertent
2c6b1a440f update glob function 2021-08-30 08:53:34 +03:00
dwertent
37afc1352f adding helm support to readme 2021-08-29 13:34:40 +03:00
dwertent
9943119033 recursive glob 2021-08-29 13:15:34 +03:00
dwertent
41457ff551 Merge remote-tracking branch 'upstream/dev' 2021-08-29 10:38:42 +03:00
dwertent
82b64b5828 Merge remote-tracking branch 'origin/dev' 2021-08-29 10:35:59 +03:00
dwertent
229e8acc74 Merge remote-tracking branch 'origin/yamlsupport' 2021-08-29 10:35:32 +03:00
David Wertenteil
30324e1c01 Merge branch 'dev' into yamlsupport 2021-08-29 10:19:09 +03:00
dwertent
8ca356eae7 Merge remote-tracking branch 'upstream/master' 2021-08-29 10:09:54 +03:00
dwertent
29f4ae368d support url input, update readme 2021-08-29 10:08:49 +03:00
dwertent
409080f51b update package name o kubescape 2021-08-29 08:17:09 +03:00
dwertent
0b24c46279 Merge remote-tracking branch 'upstream/dev' 2021-08-26 18:30:50 +03:00
dwertent
49596c5ac1 split to function 2021-08-26 18:29:32 +03:00
dwertent
9bf79db8f8 Merge branch 'Daniel-GrunbergerCA-master' into dev 2021-08-26 12:22:34 +03:00
67 changed files with 6606 additions and 634 deletions

View File

@@ -6,7 +6,6 @@ on:
pull_request:
branches: [ master ]
types: [ closed ]
jobs:
once:
name: Create release
@@ -38,11 +37,16 @@ jobs:
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Build
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape
env:
RELEASE: v1.0.${{ github.run_number }}
ArmoBEServer: api.armo.cloud
ArmoERServer: report.euprod1.cyberarmorsoft.com
ArmoWebsite: portal.armo.cloud
CGO_ENABLED: 0
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s -X github.com/armosec/kubescape/cmd.BuildNumber=$RELEASE -X github.com/armosec/kubescape/cautils/getter.ArmoBEURL=$ArmoBEServer -X github.com/armosec/kubescape/cautils/getter.ArmoERURL=$ArmoERServer -X github.com/armosec/kubescape/cautils/getter.ArmoFEURL=$ArmoWebsite" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
- name: Upload Release Asset
- name: Upload Release binaries
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:

36
.github/workflows/build_dev.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: build-dev
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
types: [ closed ]
jobs:
build:
name: Create cross-platform dev build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v1
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Build
env:
RELEASE: v1.0.${{ github.run_number }}
ArmoBEServer: api.armo.cloud
ArmoERServer: report.euprod1.cyberarmorsoft.com
ArmoWebsite: portal.armo.cloud
CGO_ENABLED: 0
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s -X github.com/armosec/kubescape/cmd.BuildNumber=$RELEASE -X github.com/armosec/kubescape/cautils/getter.ArmoBEURL=$ArmoBEServer -X github.com/armosec/kubescape/cautils/getter.ArmoERURL=$ArmoERServer -X github.com/armosec/kubescape/cautils/getter.ArmoFEURL=$ArmoWebsite" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
name: kubescape-${{ matrix.os }}
path: build/${{ matrix.os }}/kubescape

3
.gitignore vendored
View File

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

View File

@@ -1,7 +1,6 @@
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
[![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
[![Github All Releases](https://img.shields.io/github/downloads/armosec/kubescape/total.svg)](https://github.com/armosec/kubescape)
[![Go Report Card](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://goreportcard.com/report/github.com/armosec/kubescape)
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
@@ -28,20 +27,22 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
<img src="docs/summary.png">
### Flags
| flag | default | description | options |
| --- | --- | --- | --- |
| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces |
| `-s`/`--silent` | Display progress messages | Silent progress messages |
| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` |
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
| `-o`/`--output` | print to stdout | Save scan result in file |
| `--use-from` | | Load local framework object from specified path. If not used will download latest |
| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` |
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal |
| `--results-locally` | `false` | Kubescape sends scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features | `true`/`false`|
## Usage & Examples
### Pre-Deployment Testing
Check your YAML files before you're deploying, simply add them at the end of command line:
```
kubescape scan framework nsa *.yaml
```
### Integration with other tools
Kubescape can produce output fitting for later processing:
* JSON (`-f json`)
* JUnit XML (`-f junit`)
### Examples
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
@@ -49,32 +50,37 @@ Kubescape can produce output fitting for later processing:
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
* Scan local `yaml`/`json` files
* Scan local `yaml`/`json` files before deploying
```
kubescape scan framework nsa examples/online-boutique/*
kubescape scan framework nsa *.yaml
```
* Scan `yaml`/`json` files from url
* Scan `yaml`/`json` files from url
```
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
```
* Output in `json` format
* Output in `json` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
```
* Output in `junit xml` format
* Output in `junit xml` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
```
* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` <img src="docs/new-feature.svg">
```
kubescape scan framework nsa --exceptions examples/exceptions.json
```
### Helm Support
Render the helm template and pass as stdout
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
```
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
```
for example:
@@ -82,6 +88,23 @@ for example:
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
```
### Offline Support <img src="docs/new-feature.svg">
It is possible to run Kubescape offline!
First download the framework and then scan with `--use-from` flag
* Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
```
kubescape download framework nsa --output nsa.json
```
* Scan using the downloaded framework
```
kubescape scan framework nsa --use-from nsa.json
```
# How to build
Note: development (and the release process) is done with Go `1.16`
@@ -103,6 +126,18 @@ go mod tidy && go build -o kubescape .
4. Enjoy :zany_face:
# How to build in Docker
1. Clone Project
```
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
```
2. Build
```
docker build -t kubescape -f build/Dockerfile .
```
# Under the hood
## Tests
@@ -126,7 +161,9 @@ Kubescape is running the following tests according to what is defined by [Kubern
* Linux hardening
* Ingress and Egress blocked
* Container hostPort
* Anonymous requests
* Network policies
* Symlink Exchange Can Allow Host Filesystem Access (CVE-2021-25741)
## Technology

13
build/Dockerfile Normal file
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

@@ -1,5 +1,7 @@
package armotypes
import "strings"
const (
CostumerGuidQuery = "costumerGUID"
ClusterNameQuery = "cluster"
@@ -22,6 +24,7 @@ type DesignatorType string
// Supported designators
const (
DesignatorAttributes DesignatorType = "Attributes"
DesignatorAttribute DesignatorType = "Attribute" // Deprecated
/*
WorkloadID format.
k8s format: wlid://cluster-<cluster>/namespace-<namespace>/<kind>-<name>
@@ -41,10 +44,16 @@ const (
DesignatorSid DesignatorType = "Sid" // secret id
)
func (dt DesignatorType) ToLower() DesignatorType {
return DesignatorType(strings.ToLower(string(dt)))
}
// attributes
const (
AttributeCluster = "cluster"
AttributeNamespace = "namespace"
AttributeKind = "kind"
AttributeName = "name"
)
// PortalDesignator represented single designation options

View File

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

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

325
cautils/customerloader.go Normal file
View File

@@ -0,0 +1,325 @@
package cautils
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"strings"
"github.com/armosec/kubescape/cautils/getter"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/armosec/kubescape/cautils/k8sinterface"
corev1 "k8s.io/api/core/v1"
)
const (
configMapName = "kubescape"
ConfigFileName = "config"
)
type ConfigObj struct {
CustomerGUID string `json:"customerGUID"`
Token string `json:"invitationParam"`
CustomerAdminEMail string `json:"adminMail"`
}
func (co *ConfigObj) Json() []byte {
if b, err := json.Marshal(co); err == nil {
return b
}
return []byte{}
}
type IClusterConfig interface {
SetCustomerGUID() error
GetCustomerGUID() string
GenerateURL()
}
type ClusterConfig struct {
k8s *k8sinterface.KubernetesApi
defaultNS string
armoAPI *getter.ArmoAPI
configObj *ConfigObj
}
type EmptyConfig struct {
}
func (c *EmptyConfig) GenerateURL() {
}
func (c *EmptyConfig) SetCustomerGUID() error {
return nil
}
func (c *EmptyConfig) GetCustomerGUID() string {
return ""
}
func NewEmptyConfig() *EmptyConfig {
return &EmptyConfig{}
}
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, armoAPI *getter.ArmoAPI) *ClusterConfig {
return &ClusterConfig{
k8s: k8s,
armoAPI: armoAPI,
defaultNS: k8sinterface.GetDefaultNamespace(),
}
}
func createConfigJson() {
ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), nil, 0664)
}
func update(configObj *ConfigObj) {
ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), configObj.Json(), 0664)
}
func (c *ClusterConfig) GenerateURL() {
u := url.URL{}
u.Scheme = "https"
u.Host = getter.ArmoFEURL
if c.configObj == nil {
return
}
if c.configObj.CustomerAdminEMail != "" {
msgStr := fmt.Sprintf("To view all controls and get remediations ask access permissions to %s from %s", u.String(), c.configObj.CustomerAdminEMail)
InfoTextDisplay(os.Stdout, msgStr+"\n")
return
}
u.Path = "account/sign-up"
q := u.Query()
q.Add("invitationToken", c.configObj.Token)
q.Add("customerGUID", c.configObj.CustomerGUID)
u.RawQuery = q.Encode()
fmt.Println("To view all controls and get remediations visit:")
InfoTextDisplay(os.Stdout, u.String()+"\n")
}
func (c *ClusterConfig) GetCustomerGUID() string {
if c.configObj != nil {
return c.configObj.CustomerGUID
}
return ""
}
func (c *ClusterConfig) GetValueByKeyFromConfigMap(key string) (string, error) {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
if err != nil {
return "", err
}
if val, ok := configMap.Data[key]; ok {
return val, nil
} else {
return "", fmt.Errorf("value does not exist")
}
}
func GetValueFromConfigJson(key string) (string, error) {
data, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
if err != nil {
return "", err
}
var obj map[string]interface{}
err = json.Unmarshal(data, &obj)
if val, ok := obj[key]; ok {
return fmt.Sprint(val), nil
} else {
return "", fmt.Errorf("value does not exist")
}
}
func SetKeyValueInConfigJson(key string, value string) error {
data, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
if err != nil {
return err
}
var obj map[string]interface{}
err = json.Unmarshal(data, &obj)
if err != nil {
return err
}
obj[key] = value
newData, err := json.Marshal(obj)
if err != nil {
return err
}
return ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), newData, 0664)
}
func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
if err != nil {
configMap = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
},
}
}
if len(configMap.Data) == 0 {
configMap.Data = make(map[string]string)
}
configMap.Data[key] = value
if err != nil {
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
} else {
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
}
return err
}
func (c *ClusterConfig) SetCustomerGUID() error {
// get from file
if existsConfigJson() {
c.configObj, _ = loadConfigFromFile()
} else if c.existsConfigMap() {
c.configObj, _ = c.loadConfigFromConfigMap()
} else {
c.createConfigMap()
createConfigJson()
}
customerGUID := c.GetCustomerGUID()
// get from armoBE
tenantResponse, err := c.armoAPI.GetCustomerGUID(customerGUID)
if err == nil && tenantResponse != nil {
if tenantResponse.AdminMail != "" { // this customer already belongs to some user
if existsConfigJson() {
update(&ConfigObj{CustomerGUID: customerGUID, CustomerAdminEMail: tenantResponse.AdminMail})
}
if c.existsConfigMap() {
c.configObj.CustomerAdminEMail = tenantResponse.AdminMail
c.updateConfigMap()
}
} else {
if existsConfigJson() {
update(&ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token})
}
if c.existsConfigMap() {
c.configObj = &ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token}
c.updateConfigMap()
}
}
} else {
if err != nil && strings.Contains(err.Error(), "Invitation for tenant already exists") {
return nil
}
return err
}
return nil
}
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
if c.k8s == nil {
return nil, nil
}
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
if err != nil {
return nil, err
}
if bData, err := json.Marshal(configMap.Data); err == nil {
return readConfig(bData)
}
return nil, nil
}
func (c *ClusterConfig) existsConfigMap() bool {
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
return err == nil
}
func existsConfigJson() bool {
_, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
return err == nil
}
func (c *ClusterConfig) createConfigMap() error {
if c.k8s == nil {
return nil
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
},
}
c.updateConfigData(configMap)
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
return err
}
func (c *ClusterConfig) updateConfigMap() error {
if c.k8s == nil {
return nil
}
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
if err != nil {
return err
}
c.updateConfigData(configMap)
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
return err
}
func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
if len(configMap.Data) == 0 {
configMap.Data = make(map[string]string)
}
m := c.ToMapString()
for k, v := range m {
if s, ok := v.(string); ok {
configMap.Data[k] = s
}
}
}
func loadConfigFromFile() (*ConfigObj, error) {
dat, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
if err != nil {
return nil, err
}
return readConfig(dat)
}
func readConfig(dat []byte) (*ConfigObj, error) {
if len(dat) == 0 {
return nil, nil
}
configObj := &ConfigObj{}
err := json.Unmarshal(dat, configObj)
return configObj, err
}
func (c *ClusterConfig) ToMapString() map[string]interface{} {
m := map[string]interface{}{}
bc, _ := json.Marshal(c.configObj)
json.Unmarshal(bc, &m)
return m
}

View File

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

View File

@@ -21,6 +21,7 @@ func IsSilent() bool {
}
var FailureDisplay = color.New(color.Bold, color.FgHiRed).FprintfFunc()
var WarningDisplay = color.New(color.Bold, color.FgCyan).FprintfFunc()
var FailureTextDisplay = color.New(color.Faint, color.FgHiRed).FprintfFunc()
var InfoDisplay = color.New(color.Bold, color.FgHiYellow).FprintfFunc()
var InfoTextDisplay = color.New(color.Faint, color.FgHiYellow).FprintfFunc()

6
cautils/downloadinfo.go Normal file
View File

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

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

@@ -0,0 +1,88 @@
package getter
import (
"fmt"
"net/http"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// =======================================================================================================================
// =============================================== ArmoAPI ===============================================================
// =======================================================================================================================
var (
// ATTENTION!!!
// Changes in this URLs variable names, or in the usage is affecting the build process! BE CAREFULL
ArmoBEURL = "eggdashbe.eudev3.cyberarmorsoft.com"
ArmoERURL = "report.eudev3.cyberarmorsoft.com"
ArmoFEURL = "armoui.eudev3.cyberarmorsoft.com"
// ArmoURL = "https://dashbe.euprod1.cyberarmorsoft.com"
)
// Armo API for downloading policies
type ArmoAPI struct {
httpClient *http.Client
}
func NewArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{},
}
}
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return nil, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
return framework, err
}
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
exceptions := []armotypes.PostureExceptionPolicy{}
if customerGUID == "" {
return exceptions, nil
}
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName))
if err != nil {
return nil, err
}
if err = JSONDecoder(respStr).Decode(&exceptions); err != nil {
return nil, err
}
return exceptions, nil
}
func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, error) {
url := armoAPI.getCustomerURL()
if customerGUID != "" {
url = fmt.Sprintf("%s?customerGUID=%s", url, customerGUID)
}
respStr, err := HttpGetter(armoAPI.httpClient, url)
if err != nil {
return nil, err
}
tenant := &TenantResponse{}
if err = JSONDecoder(respStr).Decode(tenant); err != nil {
return nil, err
}
return tenant, nil
}
type TenantResponse struct {
TenantID string `json:"tenantId"`
Token string `json:"token"`
Expires string `json:"expires"`
AdminMail string `json:"adminMail,omitempty"`
}

View File

@@ -0,0 +1,44 @@
package getter
import (
"net/url"
"strings"
)
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = ArmoBEURL
u.Path = "v1/armoFrameworks"
q := u.Query()
q.Add("customerGUID", "11111111-1111-1111-1111-111111111111")
q.Add("frameworkName", strings.ToUpper(frameworkName))
q.Add("getRules", "true")
u.RawQuery = q.Encode()
return u.String()
}
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = ArmoBEURL
u.Path = "api/v1/armoPostureExceptions"
q := u.Query()
q.Add("customerGUID", customerGUID)
// if clusterName != "" { // TODO - fix customer name support in Armo BE
// q.Add("clusterName", clusterName)
// }
u.RawQuery = q.Encode()
return u.String()
}
func (armoAPI *ArmoAPI) getCustomerURL() string {
u := url.URL{}
u.Scheme = "https"
u.Host = ArmoBEURL
u.Path = "api/v1/createTenant"
return u.String()
}

View File

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

View File

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

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

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,16 +4,20 @@ import (
"context"
"fmt"
"os"
"strings"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
// DO NOT REMOVE - load cloud providers auth
_ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
var ConnectedToCluster = true
// K8SConfig pointer to k8s config
var K8SConfig *restclient.Config
@@ -26,8 +30,15 @@ type KubernetesApi struct {
// NewKubernetesApi -
func NewKubernetesApi() *KubernetesApi {
var kubernetesClient *kubernetes.Clientset
var err error
kubernetesClient, err := kubernetes.NewForConfig(GetK8sConfig())
if !IsConnectedToCluster() {
fmt.Println(fmt.Errorf("failed to load kubernetes config: no configuration has been provided, try setting KUBECONFIG environment variable"))
os.Exit(1)
}
kubernetesClient, err = kubernetes.NewForConfig(GetK8sConfig())
if err != nil {
fmt.Printf("Failed to load config file, reason: %s", err.Error())
os.Exit(1)
@@ -52,23 +63,54 @@ var RunningIncluster bool
func LoadK8sConfig() error {
kubeconfig, err := config.GetConfig()
if err != nil {
return fmt.Errorf("Failed to load kubernetes config: %s\n", err)
return fmt.Errorf("failed to load kubernetes config: %s", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
}
if _, err := restclient.InClusterConfig(); err == nil {
RunningIncluster = true
}
K8SConfig = kubeconfig
return nil
}
// GetK8sConfig get config. load if not loaded yer
// GetK8sConfig get config. load if not loaded yet
func GetK8sConfig() *restclient.Config {
if K8SConfig == nil {
if err := LoadK8sConfig(); err != nil {
// print error
fmt.Printf("%s", err.Error())
os.Exit(1)
}
if !IsConnectedToCluster() {
return nil
}
return K8SConfig
}
func IsConnectedToCluster() bool {
if K8SConfig == nil {
if err := LoadK8sConfig(); err != nil {
ConnectedToCluster = false
}
}
return ConnectedToCluster
}
func GetClusterName() string {
if !ConnectedToCluster {
return ""
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{})
config, err := kubeConfig.RawConfig()
if err != nil {
return ""
}
// TODO - Handle if empty
return config.CurrentContext
}
func GetDefaultNamespace() string {
clientCfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
if err != nil {
return "default"
}
namespace := clientCfg.Contexts[clientCfg.CurrentContext].Namespace
if namespace == "" {
namespace = "default"
}
return namespace
}

View File

@@ -1,7 +1,6 @@
package opapolicy
import (
"path/filepath"
"time"
armotypes "github.com/armosec/kubescape/cautils/armotypes"
@@ -17,14 +16,15 @@ const (
// RegoResponse the expected response of single run of rego policy
type RuleResponse struct {
AlertMessage string `json:"alertMessage"`
PackageName string `json:"packagename"`
AlertScore AlertScore `json:"alertScore"`
// AlertObject AlertObject `json:"alertObject"`
AlertObject AlertObject `json:"alertObject"` // TODO - replace interface to AlertObject
Context []string `json:"context"` // TODO - Remove
Rulename string `json:"rulename"` // TODO - Remove
ExceptionName string `json:"exceptionName"`
AlertMessage string `json:"alertMessage"`
RuleStatus string `json:"ruleStatus"`
PackageName string `json:"packagename"`
AlertScore AlertScore `json:"alertScore"`
AlertObject AlertObject `json:"alertObject"`
Context []string `json:"context,omitempty"` // TODO - Remove
Rulename string `json:"rulename,omitempty"` // TODO - Remove
ExceptionName string `json:"exceptionName,omitempty"` // Not in use
Exception *armotypes.PostureExceptionPolicy `json:"exception,omitempty"`
}
type AlertObject struct {
@@ -33,19 +33,27 @@ type AlertObject struct {
}
type FrameworkReport struct {
Name string `json:"name"`
ControlReports []ControlReport `json:"controlReports"`
Name string `json:"name"`
ControlReports []ControlReport `json:"controlReports"`
Score float32 `json:"score,omitempty"`
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
WCSScore float32 `json:"wcsScore,omitempty"`
}
type ControlReport struct {
Name string `json:"name"`
RuleReports []RuleReport `json:"ruleReports"`
Remediation string `json:"remediation"`
Description string `json:"description"`
armotypes.PortalBase `json:",inline"`
ControlID string `json:"id"`
Name string `json:"name"`
RuleReports []RuleReport `json:"ruleReports"`
Remediation string `json:"remediation"`
Description string `json:"description"`
Score float32 `json:"score"`
BaseScore float32 `json:"baseScore,omitempty"`
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
}
type RuleReport struct {
Name string `json:"name"`
Remediation string `json:"remediation"`
RuleStatus RuleStatus `json:"ruleStatus"`
RuleStatus RuleStatus `json:"ruleStatus"` // did we run the rule or not (if there where compile errors, the value will be failed)
RuleResponses []RuleResponse `json:"ruleResponses"`
ListInputResources []map[string]interface{} `json:"-"`
ListInputKinds []string `json:"-"`
@@ -93,6 +101,7 @@ type PolicyRule struct {
// Control represents a collection of rules which are combined together to single purpose
type Control struct {
armotypes.PortalBase `json:",inline"`
ControlID string `json:"id"`
CreationTime string `json:"creationTime"`
Description string `json:"description"`
Remediation string `json:"remediation"`
@@ -150,43 +159,3 @@ type PolicyIdentifier struct {
Kind NotificationPolicyKind `json:"kind"`
Name string `json:"name"`
}
type ScanInfo struct {
PolicyIdentifier PolicyIdentifier
Format string
Output string
ExcludedNamespaces string
InputPatterns []string
Silent bool
}
func (scanInfo *ScanInfo) Init() {
// scanInfo.setSilentMode()
scanInfo.setOutputFile()
}
func (scanInfo *ScanInfo) setSilentMode() {
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
scanInfo.Silent = true
}
if scanInfo.Output != "" {
scanInfo.Silent = true
}
}
func (scanInfo *ScanInfo) setOutputFile() {
if scanInfo.Output == "" {
return
}
if scanInfo.Format == "json" {
if filepath.Ext(scanInfo.Output) != "json" {
scanInfo.Output += ".json"
}
}
if scanInfo.Format == "junit" {
if filepath.Ext(scanInfo.Output) != "xml" {
scanInfo.Output += ".xml"
}
}
}

View File

@@ -33,7 +33,8 @@ func MockFrameworkReportA() *FrameworkReport {
Name: AMockFrameworkName,
ControlReports: []ControlReport{
{
Name: AMockControlName,
ControlID: "C-0010",
Name: AMockControlName,
RuleReports: []RuleReport{
{
Name: AMockRuleName,

View File

@@ -3,10 +3,8 @@ package opapolicy
import (
"bytes"
"encoding/json"
"fmt"
"github.com/golang/glog"
"github.com/open-policy-agent/opa/rego"
"github.com/armosec/kubescape/cautils/k8sinterface"
)
func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
@@ -17,6 +15,19 @@ func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
return bytes.NewBuffer(res), err
}
func (RuleResponse *RuleResponse) GetSingleResultStatus() string {
if RuleResponse.Exception != nil {
if RuleResponse.Exception.IsAlertOnly() {
return "warning"
}
if RuleResponse.Exception.IsDisable() {
return "ignore"
}
}
return "failed"
}
func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleResponse) {
if len(ruleReport.RuleResponses) == 0 {
return "success", nil, nil
@@ -26,9 +37,11 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
for _, rule := range ruleReport.RuleResponses {
if rule.ExceptionName != "" {
failed = append(failed, rule)
} else {
exceptions = append(exceptions, rule)
} else if rule.Exception != nil {
exceptions = append(exceptions, rule)
} else {
failed = append(failed, rule)
}
}
@@ -38,46 +51,30 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
}
return status, failed, exceptions
}
func ParseRegoResult(regoResult *rego.ResultSet) ([]RuleResponse, error) {
var errs error
ruleResponses := []RuleResponse{}
for _, result := range *regoResult {
for desicionIdx := range result.Expressions {
if resMap, ok := result.Expressions[desicionIdx].Value.(map[string]interface{}); ok {
for objName := range resMap {
jsonBytes, err := json.Marshal(resMap[objName])
if err != nil {
err = fmt.Errorf("in parseRegoResult, json.Marshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
desObj := make([]RuleResponse, 0)
if err := json.Unmarshal(jsonBytes, &desObj); err != nil {
err = fmt.Errorf("in parseRegoResult, json.Unmarshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
ruleResponses = append(ruleResponses, desObj...)
}
}
}
}
return ruleResponses, errs
}
func (controlReport *ControlReport) GetNumberOfResources() int {
sum := 0
for i := range controlReport.RuleReports {
if controlReport.RuleReports[i].ListInputResources == nil {
continue
}
sum += len(controlReport.RuleReports[i].ListInputResources)
sum += controlReport.RuleReports[i].GetNumberOfResources()
}
return sum
}
func (controlReport *ControlReport) GetNumberOfFailedResources() int {
sum := 0
for i := range controlReport.RuleReports {
sum += controlReport.RuleReports[i].GetNumberOfFailedResources()
}
return sum
}
func (controlReport *ControlReport) GetNumberOfWarningResources() int {
sum := 0
for i := range controlReport.RuleReports {
sum += controlReport.RuleReports[i].GetNumberOfWarningResources()
}
return sum
}
func (controlReport *ControlReport) ListControlsInputKinds() []string {
listControlsInputKinds := []string{}
for i := range controlReport.RuleReports {
@@ -85,15 +82,155 @@ func (controlReport *ControlReport) ListControlsInputKinds() []string {
}
return listControlsInputKinds
}
func (controlReport *ControlReport) Passed() bool {
for i := range controlReport.RuleReports {
if len(controlReport.RuleReports[i].RuleResponses) > 0 {
if len(controlReport.RuleReports[i].RuleResponses) != 0 {
return false
}
}
return true
}
func (controlReport *ControlReport) Failed() bool {
return !controlReport.Passed()
func (controlReport *ControlReport) Warning() bool {
if controlReport.Passed() || controlReport.Failed() {
return false
}
for i := range controlReport.RuleReports {
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "warning" {
return true
}
}
return false
}
func (controlReport *ControlReport) Failed() bool {
if controlReport.Passed() {
return false
}
for i := range controlReport.RuleReports {
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "failed" {
return true
}
}
return false
}
func (ruleReport *RuleReport) GetNumberOfResources() int {
return len(ruleReport.ListInputResources)
}
func (ruleReport *RuleReport) GetNumberOfFailedResources() int {
sum := 0
for i := len(ruleReport.RuleResponses) - 1; i >= 0; i-- {
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
if !ruleReport.DeleteIfRedundantResponse(&ruleReport.RuleResponses[i], i) {
sum++
}
}
}
return sum
}
func (ruleReport *RuleReport) DeleteIfRedundantResponse(RuleResponse *RuleResponse, index int) bool {
if b, rr := ruleReport.IsDuplicateResponseOfResource(RuleResponse, index); b {
rr.AddMessageToResponse(RuleResponse.AlertMessage)
ruleReport.RuleResponses = removeResponse(ruleReport.RuleResponses, index)
return true
}
return false
}
func (ruleResponse *RuleResponse) AddMessageToResponse(message string) {
ruleResponse.AlertMessage += message
}
func (ruleReport *RuleReport) IsDuplicateResponseOfResource(RuleResponse *RuleResponse, index int) (bool, *RuleResponse) {
for i := range ruleReport.RuleResponses {
if i != index {
for j := range ruleReport.RuleResponses[i].AlertObject.K8SApiObjects {
for k := range RuleResponse.AlertObject.K8SApiObjects {
w1 := k8sinterface.NewWorkloadObj(ruleReport.RuleResponses[i].AlertObject.K8SApiObjects[j])
w2 := k8sinterface.NewWorkloadObj(RuleResponse.AlertObject.K8SApiObjects[k])
if w1.GetName() == w2.GetName() && w1.GetNamespace() == w2.GetNamespace() && w1.GetKind() != "Role" && w1.GetKind() != "ClusterRole" {
return true, &ruleReport.RuleResponses[i]
}
}
}
}
}
return false, nil
}
func removeResponse(slice []RuleResponse, index int) []RuleResponse {
return append(slice[:index], slice[index+1:]...)
}
func (ruleReport *RuleReport) GetNumberOfWarningResources() int {
sum := 0
for i := range ruleReport.RuleResponses {
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "warning" {
sum += 1
}
}
return sum
}
func (postureReport *PostureReport) RemoveData() {
for i := range postureReport.FrameworkReports {
postureReport.FrameworkReports[i].RemoveData()
}
}
func (frameworkReport *FrameworkReport) RemoveData() {
for i := range frameworkReport.ControlReports {
frameworkReport.ControlReports[i].RemoveData()
}
}
func (controlReport *ControlReport) RemoveData() {
for i := range controlReport.RuleReports {
controlReport.RuleReports[i].RemoveData()
}
}
func (ruleReport *RuleReport) RemoveData() {
for i := range ruleReport.RuleResponses {
ruleReport.RuleResponses[i].RemoveData()
}
}
func (r *RuleResponse) RemoveData() {
r.AlertObject.ExternalObjects = nil
keepFields := []string{"kind", "apiVersion", "metadata"}
keepMetadataFields := []string{"name", "namespace", "labels"}
for i := range r.AlertObject.K8SApiObjects {
deleteFromMap(r.AlertObject.K8SApiObjects[i], keepFields)
for k := range r.AlertObject.K8SApiObjects[i] {
if k == "metadata" {
if b, ok := r.AlertObject.K8SApiObjects[i][k].(map[string]interface{}); ok {
deleteFromMap(b, keepMetadataFields)
r.AlertObject.K8SApiObjects[i][k] = b
}
}
}
}
}
func deleteFromMap(m map[string]interface{}, keepFields []string) {
for k := range m {
if StringInSlice(keepFields, k) {
continue
}
delete(m, k)
}
}
func StringInSlice(strSlice []string, str string) bool {
for i := range strSlice {
if strSlice[i] == str {
return true
}
}
return false
}

View File

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

88
cautils/scaninfo.go Normal file
View File

@@ -0,0 +1,88 @@
package cautils
import (
"path/filepath"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type ScanInfo struct {
Getters
PolicyIdentifier opapolicy.PolicyIdentifier
UseExceptions string
UseFrom string
UseDefault bool
Format string
Output string
ExcludedNamespaces string
InputPatterns []string
Silent bool
FailThreshold uint16
DoNotSendResults bool
}
type Getters struct {
ExceptionsGetter getter.IExceptionsGetter
PolicyGetter getter.IPolicyGetter
}
func (scanInfo *ScanInfo) Init() {
scanInfo.setUseFrom()
scanInfo.setUseExceptions()
scanInfo.setOutputFile()
scanInfo.setGetter()
}
func (scanInfo *ScanInfo) setUseExceptions() {
if scanInfo.UseExceptions != "" {
// load exceptions from file
scanInfo.ExceptionsGetter = getter.NewLoadPolicy(scanInfo.UseExceptions)
} else {
scanInfo.ExceptionsGetter = getter.NewArmoAPI()
}
}
func (scanInfo *ScanInfo) setUseFrom() {
if scanInfo.UseFrom != "" {
return
}
if scanInfo.UseDefault {
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
}
}
func (scanInfo *ScanInfo) setGetter() {
if scanInfo.UseFrom != "" {
// load from file
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
} else {
scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
}
}
func (scanInfo *ScanInfo) setOutputFile() {
if scanInfo.Output == "" {
return
}
if scanInfo.Format == "json" {
if filepath.Ext(scanInfo.Output) != ".json" {
scanInfo.Output += ".json"
}
}
if scanInfo.Format == "junit" {
if filepath.Ext(scanInfo.Output) != ".xml" {
scanInfo.Output += ".xml"
}
}
}
func (scanInfo *ScanInfo) ScanRunningCluster() bool {
return len(scanInfo.InputPatterns) == 0
}
// func (scanInfo *ScanInfo) ConnectedToCluster(k8s k8sinterface.) bool {
// _, err := k8s.KubernetesClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
// return err == nil
// }

18
cmd/cluster.go Normal file
View File

@@ -0,0 +1,18 @@
package cmd
import (
"github.com/spf13/cobra"
)
// clusterCmd represents the cluster command
var clusterCmd = &cobra.Command{
Use: "cluster",
Short: "Set configuration for cluster",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
},
}
func init() {
configCmd.AddCommand(clusterCmd)
}

50
cmd/cluster_get.go Normal file
View File

@@ -0,0 +1,50 @@
package cmd
import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/spf13/cobra"
)
var getCmd = &cobra.Command{
Use: "get <key>",
Short: "Get configuration in cluster",
Long: ``,
ValidArgs: supportedFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument")
}
keyValue := strings.Split(args[0], "=")
if len(keyValue) != 1 {
return fmt.Errorf("requires one argument")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
keyValue := strings.Split(args[0], "=")
key := keyValue[0]
k8s := k8sinterface.NewKubernetesApi()
clusterConfig := cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
val, err := clusterConfig.GetValueByKeyFromConfigMap(key)
if err != nil {
if err.Error() == "value does not exist." {
fmt.Printf("Could net get value from configmap, reason: %s\n", err)
return nil
}
return err
}
fmt.Println(key + "=" + val)
return nil
},
}
func init() {
clusterCmd.AddCommand(getCmd)
}

44
cmd/cluster_set.go Normal file
View File

@@ -0,0 +1,44 @@
package cmd
import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/spf13/cobra"
)
var setCmd = &cobra.Command{
Use: "set <key>=<value>",
Short: "Set configuration in cluster",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument: <key>=<value>")
}
keyValue := strings.Split(args[0], "=")
if len(keyValue) != 2 {
return fmt.Errorf("requires one argument: <key>=<value>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
keyValue := strings.Split(args[0], "=")
key := keyValue[0]
data := keyValue[1]
k8s := k8sinterface.NewKubernetesApi()
clusterConfig := cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
if err := clusterConfig.SetKeyValueInConfigmap(key, data); err != nil {
return err
}
fmt.Println("Value added successfully.")
return nil
},
}
func init() {
clusterCmd.AddCommand(setCmd)
}

19
cmd/config.go Normal file
View File

@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
)
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Set configuration",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
},
}
func init() {
rootCmd.AddCommand(configCmd)
}

45
cmd/download.go Normal file
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 + ".json")
}
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
if err != nil {
return err
}
err = getter.SaveFrameworkInFile(frameworks, downloadInfo.Path)
if err != nil {
return err
}
return nil
},
}
func init() {
rootCmd.AddCommand(downloadCmd)
downloadInfo = cautils.DownloadInfo{}
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If specified, will store save to `~/.kubescape/<framework name>.json`")
}

View File

@@ -10,21 +10,24 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/opaprocessor"
"github.com/armosec/kubescape/policyhandler"
"github.com/armosec/kubescape/printer"
"github.com/armosec/kubescape/resultshandling"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/reporter"
"github.com/spf13/cobra"
)
var scanInfo opapolicy.ScanInfo
var scanInfo cautils.ScanInfo
var supportedFrameworks = []string{"nsa"}
type CLIHandler struct {
policyHandler *policyhandler.PolicyHandler
scanInfo *opapolicy.ScanInfo
scanInfo *cautils.ScanInfo
}
var frameworkCmd = &cobra.Command{
@@ -33,37 +36,45 @@ var frameworkCmd = &cobra.Command{
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
ValidArgs: supportedFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
return fmt.Errorf("requires at least one argument")
}
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
} else if len(args) > 0 {
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
scanInfo.PolicyIdentifier = opapolicy.PolicyIdentifier{}
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
scanInfo.PolicyIdentifier.Name = args[0]
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
} else { // store stout to file
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
if !(cmd.Flags().Lookup("use-from").Changed) {
scanInfo.PolicyIdentifier.Name = args[0]
}
if len(args) > 0 {
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
} else { // store stout to file
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
return err
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
return err
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
CliSetup()
err := CliSetup()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return nil
},
}
@@ -74,17 +85,32 @@ func isValidFramework(framework string) bool {
func init() {
scanCmd.AddCommand(frameworkCmd)
scanInfo = opapolicy.ScanInfo{}
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "output file. print output to file and not stdout")
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "silent progress output")
scanInfo = cautils.ScanInfo{}
frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Path to load framework from")
frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load framework from default path")
frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to file containing list of exceptions")
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout")
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages")
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1")
frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armosec backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features")
}
func CliSetup() error {
flag.Parse()
k8s := k8sinterface.NewKubernetesApi()
if 100 < scanInfo.FailThreshold {
fmt.Println("bad argument: out of range threshold")
os.Exit(1)
}
var k8s *k8sinterface.KubernetesApi
if !scanInfo.ScanRunningCluster() {
k8sinterface.ConnectedToCluster = false
} else {
k8s = k8sinterface.NewKubernetesApi()
}
processNotification := make(chan *cautils.OPASessionObj)
reportResults := make(chan *cautils.OPASessionObj)
@@ -92,19 +118,45 @@ func CliSetup() error {
// policy handler setup
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
// cli handler setup
cli := NewCLIHandler(policyHandler)
if err := cli.Scan(); err != nil {
panic(err)
// load cluster config
var clusterConfig cautils.IClusterConfig
if !scanInfo.DoNotSendResults && k8sinterface.ConnectedToCluster {
clusterConfig = cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
} else {
clusterConfig = cautils.NewEmptyConfig()
}
if err := clusterConfig.SetCustomerGUID(); err != nil {
fmt.Println(err)
}
cautils.CustomerGUID = clusterConfig.GetCustomerGUID()
cautils.ClusterName = k8sinterface.GetClusterName()
// cli handler setup
go func() {
cli := NewCLIHandler(policyHandler)
if err := cli.Scan(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}()
// processor setup - rego run
go func() {
reporterObj := opaprocessor.NewOPAProcessor(&processNotification, &reportResults)
reporterObj.ProcessRulesListenner()
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
opaprocessorObj.ProcessRulesListenner()
}()
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
p.ActionPrint()
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
score := resultsHandling.HandleResults()
// print report url
clusterConfig.GenerateURL()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {
return fmt.Errorf("Scan score is bellow threshold")
}
return nil
}
@@ -127,12 +179,10 @@ func (clihandler *CLIHandler) Scan() error {
}
switch policyNotification.NotificationType {
case opapolicy.TypeExecPostureScan:
go func() {
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
fmt.Printf("%v\n", err)
os.Exit(0)
}
}()
//
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
return err
}
default:
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
}

17
cmd/local.go Normal file
View File

@@ -0,0 +1,17 @@
package cmd
import (
"github.com/spf13/cobra"
)
var localCmd = &cobra.Command{
Use: "local",
Short: "Set configuration locally (for config.json)",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
},
}
func init() {
configCmd.AddCommand(localCmd)
}

46
cmd/local_get.go Normal file
View File

@@ -0,0 +1,46 @@
package cmd
import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/spf13/cobra"
)
var localGetCmd = &cobra.Command{
Use: "get <key>",
Short: "Get configuration locally",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument")
}
keyValue := strings.Split(args[0], "=")
if len(keyValue) != 1 {
return fmt.Errorf("requires one argument")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
keyValue := strings.Split(args[0], "=")
key := keyValue[0]
val, err := cautils.GetValueFromConfigJson(key)
if err != nil {
if err.Error() == "value does not exist." {
fmt.Printf("Could net get value from: %s, reason: %s\n", getter.GetDefaultPath(cautils.ConfigFileName+".json"), err)
return nil
}
return err
}
fmt.Println(key + "=" + val)
return nil
},
}
func init() {
localCmd.AddCommand(localGetCmd)
}

40
cmd/local_set.go Normal file
View File

@@ -0,0 +1,40 @@
package cmd
import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/spf13/cobra"
)
var localSetCmd = &cobra.Command{
Use: "set <key>=<value>",
Short: "Set configuration locally",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument: <key>=<value>")
}
keyValue := strings.Split(args[0], "=")
if len(keyValue) != 2 {
return fmt.Errorf("requires one argument: <key>=<value>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
keyValue := strings.Split(args[0], "=")
key := keyValue[0]
data := keyValue[1]
if err := cautils.SetKeyValueInConfigJson(key, data); err != nil {
return err
}
fmt.Println("Value added successfully.")
return nil
},
}
func init() {
localCmd.AddCommand(localSetCmd)
}

View File

@@ -9,7 +9,7 @@ var cfgFile string
var rootCmd = &cobra.Command{
Use: "kubescape",
Short: "Kubescape is a tool for testing Kubernetes security posture",
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA and MITRE specifications.`,
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA specifications.`,
}
func Execute() {

49
cmd/version.go Normal file
View File

@@ -0,0 +1,49 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/spf13/cobra"
)
var BuildNumber string
var versionCmd = &cobra.Command{
Use: "version",
Short: "Get current version",
Long: ``,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Your current version is: " + BuildNumber)
return nil
},
}
func GetLatestVersion() (string, error) {
latestVersion := "https://api.github.com/repos/armosec/kubescape/releases/latest"
resp, err := http.Get(latestVersion)
if err != nil {
return "", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode < 200 || 301 < resp.StatusCode {
return "", fmt.Errorf("failed to download file, status code: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body from '%s', reason: %s", latestVersion, err.Error())
}
var data map[string]interface{}
err = json.Unmarshal(body, &data)
if err != nil {
return "", fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestVersion, err.Error())
}
return fmt.Sprintf("%v", data["tag_name"]), nil
}
func init() {
rootCmd.AddCommand(versionCmd)
}

View File

@@ -50,6 +50,18 @@ kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatfo
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
```
### Scan on-prem (offline)
* Scan using a framework from the local file system
```
kubescape scan framework --use-from <path>
```
* Scan using the framework from the default location in file system
```
kubescape scan framework --use-default
```
## Output formats
By default, the output is user friendly.
@@ -64,4 +76,16 @@ kubescape scan framework nsa --format json --output results.json
* Output in `junit xml` format <img src="new-feature.svg">
```
kubescape scan framework nsa --format junit --output results.xml
```
```
## Download
* Download and save in file <img src="new-feature.svg">
```
kubescape download framework nsa --output nsa.json
```
* Download and save in default file (`~/.kubescape/<framework name>.json`)
```
kubescape download framework nsa
```

BIN
docs/summary.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 65 KiB

34
examples/exceptions.json Normal file
View File

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

View File

@@ -1,7 +1,7 @@
#!/bin/bash
set -e
echo "Installing Kubescape..."
echo -e "\033[0;36mInstalling Kubescape..."
echo
BASE_DIR=~/.kubescape
@@ -27,15 +27,38 @@ mkdir -p $BASE_DIR
OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
echo -e "\033[32m[V] Downloaded Kubescape"
sudo chmod +x $OUTPUT
sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
sudo cp $OUTPUT /usr/local/bin
rm -rf $BASE_DIR
# Checking if SUDO needed/exists
SUDO=
if [ "$(id -u)" -ne 0 ] && [ -n "$(which sudo)" ]; then
SUDO=sudo
fi
echo -e "[V] Finished Installation"
# Find install dir
install_dir=/usr/local/bin #default
for pdir in ${PATH//:/ }; do
edir="${pdir/#\~/$HOME}"
if [[ $edir == $HOME/* ]]; then
install_dir=$edir
mkdir -p $install_dir 2>/dev/null || true
SUDO=
break
fi
done
chmod +x $OUTPUT 2>/dev/null
$SUDO rm -f /usr/local/bin/$KUBESCAPE_EXEC 2>/dev/null || true # clearning up old install
$SUDO cp $OUTPUT $install_dir/$KUBESCAPE_EXEC
rm -rf $OUTPUT
echo
echo -e "\033[32mFinished Installation."
echo -e "\033[0m"
$KUBESCAPE_EXEC version
echo
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
echo
echo -e "\033[35mUsage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
echo -e "\033[0m"

18
main.go
View File

@@ -1,7 +1,23 @@
package main
import "github.com/armosec/kubescape/cmd"
import (
"fmt"
"os"
"github.com/armosec/kubescape/cmd"
)
func main() {
CheckLatestVersion()
cmd.Execute()
}
func CheckLatestVersion() {
latest, err := cmd.GetLatestVersion()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
} else if latest != cmd.BuildNumber {
fmt.Println("Warning: You are not updated to the latest release: " + latest)
}
}

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ const (
JSON_FILE_FORMAT FileFormat = "json"
)
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
workloads := []k8sinterface.IWorkload{}
// load resource from local file system

View File

@@ -5,6 +5,7 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
@@ -15,6 +16,7 @@ type PolicyHandler struct {
k8s *k8sinterface.KubernetesApi
// we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func
processPolicy *chan *cautils.OPASessionObj
getters *cautils.Getters
}
// CreatePolicyHandler Create ws-handler obj
@@ -25,13 +27,14 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
}
}
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *opapolicy.ScanInfo) error {
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *cautils.ScanInfo) error {
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
// validate notification
// TODO
policyHandler.getters = &scanInfo.Getters
// get policies
frameworks, err := policyHandler.getPolicies(notification)
frameworks, exceptions, err := policyHandler.getPolicies(notification)
if err != nil {
return err
}
@@ -39,6 +42,7 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return fmt.Errorf("empty list of frameworks")
}
opaSessionObj.Frameworks = frameworks
opaSessionObj.Exceptions = exceptions
k8sResources, err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
if err != nil {
@@ -54,33 +58,31 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return nil
}
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
cautils.ProgressTextDisplay("Downloading framework definitions")
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
// TODO - support load policies from local file
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
frameworks, exceptions, err := policyHandler.GetPoliciesFromBackend(notification)
if err != nil {
return frameworks, err
return frameworks, exceptions, err
}
if len(frameworks) == 0 {
err := fmt.Errorf("could not download any policies, please check previous logs")
return frameworks, err
return frameworks, exceptions, err
}
cautils.SuccessTextDisplay("Downloaded framework")
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
return frameworks, nil
return frameworks, exceptions, nil
}
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
var k8sResources *cautils.K8SResources
var err error
if len(scanInfo.InputPatterns) > 0 {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
} else {
if k8sinterface.ConnectedToCluster {
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, &notification.Designators, scanInfo.ExcludedNamespaces)
} else {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
}
return k8sResources, err

View File

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

View File

@@ -9,7 +9,6 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/enescakir/emoji"
@@ -27,58 +26,74 @@ const (
)
type Printer struct {
opaSessionObj *chan *cautils.OPASessionObj
writer *os.File
summary Summary
sortedControlNames []string
printerType string
}
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj, printerType, outputFile string) *Printer {
func NewPrinter(printerType, outputFile string) *Printer {
return &Printer{
opaSessionObj: opaSessionObj,
summary: NewSummary(),
printerType: printerType,
writer: getWriter(outputFile),
summary: NewSummary(),
writer: getWriter(outputFile),
printerType: printerType,
}
}
func (printer *Printer) ActionPrint() {
for {
opaSessionObj := <-*printer.opaSessionObj
if printer.printerType == PrettyPrinter {
printer.SummarySetup(opaSessionObj.PostureReport)
printer.PrintResults()
printer.PrintSummaryTable()
} else if printer.printerType == JsonPrinter {
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
totalResources := 0
totalFailed := 0
for _, frameworkReport := range postureReport.FrameworkReports {
for _, controlReport := range frameworkReport.ControlReports {
for _, ruleReport := range controlReport.RuleReports {
for _, ruleResponses := range ruleReport.RuleResponses {
totalFailed += len(ruleResponses.AlertObject.K8SApiObjects)
totalFailed += len(ruleResponses.AlertObject.ExternalObjects)
}
}
printer.writer.Write(postureReportStr)
} else if printer.printerType == JunitResultPrinter {
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
printer.writer.Write(postureReportStr)
} else if !cautils.IsSilent() {
fmt.Println("unknown output printer")
os.Exit(1)
}
if !k8sinterface.RunningIncluster {
break
totalResources += controlReport.GetNumberOfResources()
}
}
if totalResources == 0 {
return float32(0)
}
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
}
func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 {
var score float32
if printer.printerType == PrettyPrinter {
printer.SummarySetup(opaSessionObj.PostureReport)
printer.PrintResults()
printer.PrintSummaryTable()
} else if printer.printerType == JsonPrinter {
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
printer.writer.Write(postureReportStr)
} else if printer.printerType == JunitResultPrinter {
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
printer.writer.Write(postureReportStr)
} else if !cautils.IsSilent() {
fmt.Println("unknown output printer")
os.Exit(1)
}
score = calculatePostureScore(opaSessionObj.PostureReport)
return score
}
func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
@@ -92,7 +107,8 @@ func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
printer.summary[cr.Name] = ControlSummary{
TotalResources: cr.GetNumberOfResources(),
TotalFailed: len(workloadsSummary),
TotalFailed: cr.GetNumberOfFailedResources(),
TotalWarnign: cr.GetNumberOfWarningResources(),
WorkloadSummary: mapResources,
Description: cr.Description,
Remediation: cr.Remediation,
@@ -101,9 +117,7 @@ func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
}
}
printer.sortedControlNames = printer.getSortedControlsNames()
}
func (printer *Printer) PrintResults() {
for i := 0; i < len(printer.sortedControlNames); i++ {
controlSummary := printer.summary[printer.sortedControlNames[i]]
@@ -120,6 +134,7 @@ func (printer *Printer) PrintResults() {
func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
cautils.SimpleDisplay(printer.writer, "Summary - ")
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed)
cautils.WarningDisplay(printer.writer, "Warning:%v ", controlSummary.TotalWarnign)
cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
if controlSummary.TotalFailed > 0 {
@@ -133,10 +148,12 @@ func (printer *Printer) printTitle(controlName string, controlSummary *ControlSu
cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName)
if controlSummary.TotalResources == 0 && len(controlSummary.ListInputKinds) > 0 {
cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace)
} else if controlSummary.TotalFailed == 0 {
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
} else {
} else if controlSummary.TotalFailed != 0 {
cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
} else if controlSummary.TotalWarnign != 0 {
cautils.WarningDisplay(printer.writer, "warning %v\n", emoji.NeutralFace)
} else {
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
}
cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
@@ -161,6 +178,10 @@ func (printer *Printer) printResult(controlName string, controlSummary *ControlS
}
func (printer *Printer) PrintUrl(url string) {
cautils.InfoTextDisplay(printer.writer, url)
}
func generateRow(control string, cs ControlSummary) []string {
row := []string{control}
row = append(row, cs.ToSlice()...)
@@ -173,7 +194,7 @@ func generateRow(control string, cs ControlSummary) []string {
}
func generateHeader() []string {
return []string{"Control Name", "Failed Resources", "All Resources", "% success"}
return []string{"Control Name", "Failed Resources", "Warning Resources", "All Resources", "% success"}
}
func percentage(big, small int) int {
@@ -185,11 +206,12 @@ func percentage(big, small int) int {
}
return int(float64(float64(big-small)/float64(big)) * 100)
}
func generateFooter(numControlers, sumFailed, sumTotal int) []string {
func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string {
// Control name | # failed resources | all resources | % success
row := []string{}
row = append(row, fmt.Sprintf("%d", numControlers))
row = append(row, fmt.Sprintf("%d", sumFailed))
row = append(row, fmt.Sprintf("%d", sumWarning))
row = append(row, fmt.Sprintf("%d", sumTotal))
if sumTotal != 0 {
row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
@@ -206,14 +228,16 @@ func (printer *Printer) PrintSummaryTable() {
summaryTable.SetAlignment(tablewriter.ALIGN_LEFT)
sumTotal := 0
sumFailed := 0
sumWarning := 0
for i := 0; i < len(printer.sortedControlNames); i++ {
controlSummary := printer.summary[printer.sortedControlNames[i]]
summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
sumFailed += controlSummary.TotalFailed
sumWarning += controlSummary.TotalWarnign
sumTotal += controlSummary.TotalResources
}
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, sumTotal))
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, sumWarning, sumTotal))
summaryTable.Render()
}
@@ -227,7 +251,7 @@ func (printer *Printer) getSortedControlsNames() []string {
}
func getWriter(outputFile string) *os.File {
os.Remove(outputFile)
if outputFile != "" {
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {

View File

@@ -2,6 +2,8 @@ package printer
import (
"fmt"
"github.com/armosec/kubescape/cautils/armotypes"
)
type Summary map[string]ControlSummary
@@ -13,6 +15,7 @@ func NewSummary() Summary {
type ControlSummary struct {
TotalResources int
TotalFailed int
TotalWarnign int
Description string
Remediation string
ListInputKinds []string
@@ -24,11 +27,13 @@ type WorkloadSummary struct {
Name string
Namespace string
Group string
Exception *armotypes.PostureExceptionPolicy
}
func (controlSummary *ControlSummary) ToSlice() []string {
s := []string{}
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarnign))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
return s
}

View File

@@ -34,6 +34,7 @@ func listResultSummary(ruleReports []opapolicy.RuleReport) []WorkloadSummary {
// add resource only once
for i := range resource {
resource[i].Exception = ruleReport.Exception
if ok := track[resource[i].ToString()]; !ok {
track[resource[i].ToString()] = true
workloadsSummary = append(workloadsSummary, resource[i])
@@ -51,6 +52,7 @@ func ruleResultSummary(obj opapolicy.AlertObject) ([]WorkloadSummary, error) {
if err != nil {
return resource, err
}
resource = append(resource, *r)
}

View File

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

View File

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

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

View File

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

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