mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-19 20:39:56 +00:00
Compare commits
531 Commits
test-pr
...
update-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd8bf9511d | ||
|
|
9d876b14e9 | ||
|
|
895233630f | ||
|
|
423d9c5c1f | ||
|
|
3f3681a4cd | ||
|
|
d6ccc37640 | ||
|
|
3b6bc00b03 | ||
|
|
8984f941ab | ||
|
|
46eb266064 | ||
|
|
0f2125817b | ||
|
|
1225540590 | ||
|
|
0e4ff13276 | ||
|
|
5fed9cc507 | ||
|
|
06241fce03 | ||
|
|
2b91023c6b | ||
|
|
082edf52d9 | ||
|
|
be250ff090 | ||
|
|
d74803af28 | ||
|
|
893bb86035 | ||
|
|
314a74b817 | ||
|
|
997bc2d23b | ||
|
|
5d1699291a | ||
|
|
bfca19bf25 | ||
|
|
1b94d27fd6 | ||
|
|
acf7ad04ed | ||
|
|
0f5775065e | ||
|
|
66fbca8f24 | ||
|
|
1f8de23a65 | ||
|
|
89478eabcc | ||
|
|
6be9aec5b0 | ||
|
|
32551275ba | ||
|
|
4ee6238244 | ||
|
|
54dda8bf31 | ||
|
|
270b3b320d | ||
|
|
fa17ca26e1 | ||
|
|
66e970a3dc | ||
|
|
d10d08c02b | ||
|
|
8d7c595a76 | ||
|
|
621ffd3ead | ||
|
|
5dee6d0e4f | ||
|
|
f516853af8 | ||
|
|
0d01329683 | ||
|
|
5a0f5f98c1 | ||
|
|
771fc4acca | ||
|
|
68a9d0cf60 | ||
|
|
d2bc957500 | ||
|
|
d20ec9e471 | ||
|
|
d3824028c5 | ||
|
|
5013f91814 | ||
|
|
41e47c3ad3 | ||
|
|
acfe986863 | ||
|
|
abafa9eafa | ||
|
|
dce1ee4dc6 | ||
|
|
62a143326c | ||
|
|
d72a6005bb | ||
|
|
adb9b80442 | ||
|
|
cb7cca7b44 | ||
|
|
f38bec9314 | ||
|
|
fff663bed4 | ||
|
|
6a72851182 | ||
|
|
e4962fe934 | ||
|
|
dac3af19a3 | ||
|
|
0b44e94b67 | ||
|
|
df37457504 | ||
|
|
f88a374b6d | ||
|
|
47442f954c | ||
|
|
efbb8e8367 | ||
|
|
60d7276de3 | ||
|
|
c05427ff38 | ||
|
|
3e245da02b | ||
|
|
cc7aae470f | ||
|
|
8d59a6074e | ||
|
|
1f7dd6e5f5 | ||
|
|
bf5ca3c1f0 | ||
|
|
a8574c61ea | ||
|
|
6f9c0ae85f | ||
|
|
be2c74e48a | ||
|
|
68da73855f | ||
|
|
5b3f2d0ff9 | ||
|
|
02637c7a8e | ||
|
|
7d5b374f9d | ||
|
|
1dd6d7a1b3 | ||
|
|
6b80b85555 | ||
|
|
d88bc067e2 | ||
|
|
ba78527c80 | ||
|
|
4c8692bf8c | ||
|
|
742e3bb67f | ||
|
|
a39f36c9fb | ||
|
|
9bc29032e1 | ||
|
|
a4a290a3ce | ||
|
|
379a3fbc27 | ||
|
|
a46098c034 | ||
|
|
440f39ba3e | ||
|
|
b6a4e282f9 | ||
|
|
8deff34d12 | ||
|
|
acc9b54b2b | ||
|
|
1ffa29fbaa | ||
|
|
2ae30a8162 | ||
|
|
0ca5378c6b | ||
|
|
f51a1281f7 | ||
|
|
5469d8bc04 | ||
|
|
bd7c0c580e | ||
|
|
154fec1385 | ||
|
|
5c2275e32a | ||
|
|
2da4736201 | ||
|
|
aefafeae6f | ||
|
|
1772b38b8c | ||
|
|
c844f42208 | ||
|
|
b86d051998 | ||
|
|
aaa8d1ed35 | ||
|
|
441d16aa08 | ||
|
|
b33f1c8cc7 | ||
|
|
4929af510e | ||
|
|
f28bb11c55 | ||
|
|
8bff4a02e1 | ||
|
|
33d1e018ec | ||
|
|
0c74599314 | ||
|
|
c23b85cc84 | ||
|
|
aab10d14a2 | ||
|
|
2fcbe54e4e | ||
|
|
078d154ab8 | ||
|
|
cc9dcf827e | ||
|
|
76943d05fb | ||
|
|
621ac111cb | ||
|
|
3f80bce811 | ||
|
|
cc6895fc50 | ||
|
|
7d9d8e4b59 | ||
|
|
f8d4bf515d | ||
|
|
774ebe4a5f | ||
|
|
45a07a8046 | ||
|
|
ff96edae4d | ||
|
|
34b82cad27 | ||
|
|
1a4c979ab8 | ||
|
|
3481af4a5b | ||
|
|
71dc7a702c | ||
|
|
6d92389285 | ||
|
|
bd0be45c0b | ||
|
|
7ebf078d0c | ||
|
|
1bd729cf83 | ||
|
|
88b9b22bca | ||
|
|
182162d521 | ||
|
|
1c02191bb1 | ||
|
|
ca66ccb33d | ||
|
|
07eda20b88 | ||
|
|
108c84d97d | ||
|
|
35e7fa2b94 | ||
|
|
abb7917b29 | ||
|
|
31ba56a0cf | ||
|
|
b3efe4d003 | ||
|
|
5faade2b66 | ||
|
|
79207f66be | ||
|
|
af39f9a7ef | ||
|
|
482b7c1f67 | ||
|
|
82e2fd0be2 | ||
|
|
6eec751027 | ||
|
|
4a6480c8b4 | ||
|
|
a51bfa4c3e | ||
|
|
2a48af3c17 | ||
|
|
ffeb4577e3 | ||
|
|
b5c7422355 | ||
|
|
e41b5d77a0 | ||
|
|
5afaae8847 | ||
|
|
011fc0689d | ||
|
|
db30020c95 | ||
|
|
c5341a356b | ||
|
|
85a7f57373 | ||
|
|
cd9ebdf08f | ||
|
|
bc602a78ab | ||
|
|
a2361fd155 | ||
|
|
aa8d41fc2e | ||
|
|
5bd4beb41f | ||
|
|
dbf21dee37 | ||
|
|
be49d9b7be | ||
|
|
7a5699fba3 | ||
|
|
1f8afecea8 | ||
|
|
3ebb1d749e | ||
|
|
f80c9d947d | ||
|
|
03b76ff4aa | ||
|
|
01531b6276 | ||
|
|
aedfe1c4c0 | ||
|
|
d2bedc1d2b | ||
|
|
35288e7b85 | ||
|
|
cd046fa695 | ||
|
|
407b8be08f | ||
|
|
b211fe9148 | ||
|
|
525e51d68e | ||
|
|
daabd6c81a | ||
|
|
5b351d5eec | ||
|
|
a5b607ae2e | ||
|
|
fec51b00ba | ||
|
|
4f9809eec1 | ||
|
|
c0c25c3430 | ||
|
|
6ed3e408be | ||
|
|
6042818a71 | ||
|
|
b37c20aed9 | ||
|
|
3de8204c43 | ||
|
|
d5bd3708b8 | ||
|
|
2bd686131e | ||
|
|
1ea4e0c304 | ||
|
|
b3251306d0 | ||
|
|
91ecdaba4e | ||
|
|
fa05dcd00d | ||
|
|
1c2c928732 | ||
|
|
69ac490006 | ||
|
|
b67b9f3af2 | ||
|
|
628ed4a374 | ||
|
|
a6fe34b466 | ||
|
|
2e9406d96a | ||
|
|
6b1bf07f7f | ||
|
|
318c2c7ae6 | ||
|
|
45f60b6fe0 | ||
|
|
20557bc721 | ||
|
|
d8bfb27bc3 | ||
|
|
9776691816 | ||
|
|
80e419df24 | ||
|
|
143f831f5b | ||
|
|
a4897304e8 | ||
|
|
80d1165e2c | ||
|
|
31ed7d5160 | ||
|
|
d62e9ce207 | ||
|
|
3b10443ff5 | ||
|
|
218e3914b1 | ||
|
|
2b07fbe782 | ||
|
|
0655d0496b | ||
|
|
02bf31fbc9 | ||
|
|
4d8a3f51e3 | ||
|
|
43d29f7b8b | ||
|
|
ee0d4cba98 | ||
|
|
d860c0234a | ||
|
|
f13ded61bf | ||
|
|
fe8fc700f4 | ||
|
|
47b670637b | ||
|
|
97c83a652b | ||
|
|
a089831720 | ||
|
|
e4f1720a0c | ||
|
|
233eb2134c | ||
|
|
06da926455 | ||
|
|
37f6193fe0 | ||
|
|
27ac036b7d | ||
|
|
321d335b39 | ||
|
|
91b7d8fc2b | ||
|
|
2b28911db0 | ||
|
|
667e5e8258 | ||
|
|
728b341048 | ||
|
|
75b295d579 | ||
|
|
75298eabf2 | ||
|
|
2458f2ceb9 | ||
|
|
f57948ad97 | ||
|
|
d0befc5f16 | ||
|
|
5d4bd2e94e | ||
|
|
ae37fdc295 | ||
|
|
3dd95ff3a3 | ||
|
|
daadb5b804 | ||
|
|
d250017faf | ||
|
|
835bcbeb12 | ||
|
|
2e4f7c4477 | ||
|
|
66bf93eb0c | ||
|
|
3a036ed0e3 | ||
|
|
fe7dad4560 | ||
|
|
fb36b09f3a | ||
|
|
e71b0c75a9 | ||
|
|
d615099ce1 | ||
|
|
f265b91939 | ||
|
|
825694ade1 | ||
|
|
979a30aea7 | ||
|
|
39c4aa4faa | ||
|
|
475b672a7a | ||
|
|
815c87b532 | ||
|
|
82120f9d31 | ||
|
|
0545818f82 | ||
|
|
046da1940c | ||
|
|
a31154897f | ||
|
|
199c57be30 | ||
|
|
7d55c79f11 | ||
|
|
ee76364371 | ||
|
|
4f2c7ac1de | ||
|
|
00340827be | ||
|
|
708fe64240 | ||
|
|
8985bbe3a9 | ||
|
|
1ffca5648e | ||
|
|
76b1ecb022 | ||
|
|
fc69a3692e | ||
|
|
e159458129 | ||
|
|
b259f117ff | ||
|
|
13cf34bffd | ||
|
|
0300fee38b | ||
|
|
d61d641e81 | ||
|
|
2added0f7c | ||
|
|
b6f6573ed8 | ||
|
|
4215771134 | ||
|
|
fd37446e1b | ||
|
|
351498aac5 | ||
|
|
2005010568 | ||
|
|
e16c4cc9b4 | ||
|
|
544ba9831a | ||
|
|
b6c919feb1 | ||
|
|
1c3b2831a2 | ||
|
|
8a19a73bb1 | ||
|
|
d966b0acbc | ||
|
|
14ffe35437 | ||
|
|
985d72e5fb | ||
|
|
70a9380966 | ||
|
|
f706d126f5 | ||
|
|
600f19406e | ||
|
|
d7ebf3239b | ||
|
|
5e0b25b04a | ||
|
|
98fe2347fa | ||
|
|
9b22d3284e | ||
|
|
9544e9cd66 | ||
|
|
1ed1bb11f2 | ||
|
|
b8ca1fcbce | ||
|
|
326a3e4c63 | ||
|
|
b348acd291 | ||
|
|
4fc3eacf7b | ||
|
|
d6030a9c03 | ||
|
|
e87bf7b723 | ||
|
|
4ef0b27ccf | ||
|
|
219582b92a | ||
|
|
07ed8c61f1 | ||
|
|
c585abc21a | ||
|
|
08696c583a | ||
|
|
7d94dc74bb | ||
|
|
570369a66f | ||
|
|
97f24920e8 | ||
|
|
f57305280f | ||
|
|
53c134cbc3 | ||
|
|
3c3a1838e3 | ||
|
|
adfd09a9d4 | ||
|
|
43ac47ec51 | ||
|
|
ec715ab68b | ||
|
|
fbff5873f7 | ||
|
|
a81eab0a1a | ||
|
|
cfc52856b3 | ||
|
|
5707d7f7e4 | ||
|
|
4f3ef49f99 | ||
|
|
a9ac880356 | ||
|
|
761d4c6ff4 | ||
|
|
bbb2aafc7e | ||
|
|
7735087937 | ||
|
|
5b9c6491de | ||
|
|
b0e3744140 | ||
|
|
0451cdb345 | ||
|
|
4546465f4a | ||
|
|
52c564b2a4 | ||
|
|
0abc81003e | ||
|
|
817d4902ff | ||
|
|
5553a1adf0 | ||
|
|
e95352d31e | ||
|
|
5655051a95 | ||
|
|
90c359533f | ||
|
|
7013d83aa8 | ||
|
|
c4935671fe | ||
|
|
e67ef1c54d | ||
|
|
5d6b9a5e83 | ||
|
|
a6752a5a6d | ||
|
|
6f47f8dae0 | ||
|
|
d82e8daa25 | ||
|
|
1cad446fe6 | ||
|
|
12c6f34fe7 | ||
|
|
79c2dd9a2b | ||
|
|
f64d5a03af | ||
|
|
e27d1581f1 | ||
|
|
99985e3235 | ||
|
|
27782afeac | ||
|
|
0e846b2dc5 | ||
|
|
45fb0f207e | ||
|
|
b7c91c238d | ||
|
|
76635a0705 | ||
|
|
c91d69e7fd | ||
|
|
5fd7096d67 | ||
|
|
f4189cb5ec | ||
|
|
278ca5b8ae | ||
|
|
729efcb8c3 | ||
|
|
406031d4e9 | ||
|
|
9e1d0d2cd6 | ||
|
|
f3e78f9408 | ||
|
|
eea4cc0b49 | ||
|
|
1127f44c10 | ||
|
|
377509fab8 | ||
|
|
c21e2f3147 | ||
|
|
0b4c5db939 | ||
|
|
6d490fc501 | ||
|
|
dbb71ba066 | ||
|
|
d5b8532e40 | ||
|
|
db396b26f8 | ||
|
|
1242259331 | ||
|
|
ad0e50898a | ||
|
|
3cf45cffd8 | ||
|
|
ac0d982531 | ||
|
|
99e22efe7b | ||
|
|
aedeb8f9cb | ||
|
|
824e76200e | ||
|
|
8342f96a62 | ||
|
|
b824d52345 | ||
|
|
11b6567db4 | ||
|
|
c7d3105ca5 | ||
|
|
f1c15cd2b5 | ||
|
|
7507f58306 | ||
|
|
48ad56a2ef | ||
|
|
2fdec20b28 | ||
|
|
2d77ea7b62 | ||
|
|
eacd559c34 | ||
|
|
c56e5799d7 | ||
|
|
ae5744f54e | ||
|
|
c649cc66a5 | ||
|
|
7db735ade6 | ||
|
|
456145e240 | ||
|
|
382a2f03c8 | ||
|
|
44ebf59d76 | ||
|
|
0688e3620b | ||
|
|
ab534b0346 | ||
|
|
09420a41a7 | ||
|
|
e93eb942a8 | ||
|
|
12f87b2710 | ||
|
|
d6dc8f219c | ||
|
|
fb3376d305 | ||
|
|
ef2ded1933 | ||
|
|
e9f1d4085a | ||
|
|
51a9707d24 | ||
|
|
a4058eac62 | ||
|
|
f2b621134c | ||
|
|
58ce50e751 | ||
|
|
2bbedc99dd | ||
|
|
78794990d7 | ||
|
|
a7127c0b27 | ||
|
|
01505406a6 | ||
|
|
e1fe7cda50 | ||
|
|
f0bc2845cf | ||
|
|
c2c521b715 | ||
|
|
2d5ea3e789 | ||
|
|
137fe81701 | ||
|
|
f293606f81 | ||
|
|
d6d2315ad0 | ||
|
|
65aa28dd38 | ||
|
|
15e55e011c | ||
|
|
0ee98351c0 | ||
|
|
f52056a879 | ||
|
|
840162c865 | ||
|
|
160709eabf | ||
|
|
7f9f6d35f7 | ||
|
|
b2b37f6abc | ||
|
|
0863d845e1 | ||
|
|
da6faa3df0 | ||
|
|
3cbd2c458d | ||
|
|
629451dd33 | ||
|
|
29a313e708 | ||
|
|
38896ccd24 | ||
|
|
834623762d | ||
|
|
c937ed16f4 | ||
|
|
ea5f72af4e | ||
|
|
beb5a4d43e | ||
|
|
77e21d5e94 | ||
|
|
3fd7bf40cc | ||
|
|
18e0a227e1 | ||
|
|
060c17b480 | ||
|
|
e67a2e9d1c | ||
|
|
dfa5f1037e | ||
|
|
a15fc066e1 | ||
|
|
effc57dfda | ||
|
|
4b5c2dfed4 | ||
|
|
f39d4efd62 | ||
|
|
97ce466fbd | ||
|
|
a94dc85e14 | ||
|
|
7811b0a4a6 | ||
|
|
f9cc9b5b28 | ||
|
|
2f208c0866 | ||
|
|
97e4ca749b | ||
|
|
9521cf1974 | ||
|
|
8ec56976c5 | ||
|
|
5993f2db3a | ||
|
|
d0abfb4ae7 | ||
|
|
bd35d521f2 | ||
|
|
533c0392d4 | ||
|
|
e0f2944fc8 | ||
|
|
4c9cacecfe | ||
|
|
6ee6a78a75 | ||
|
|
e754ecff4f | ||
|
|
bda7a17f41 | ||
|
|
dee6ed96f8 | ||
|
|
0d1de027c9 | ||
|
|
22c85b5e3b | ||
|
|
d27284b6f6 | ||
|
|
4bde684d8a | ||
|
|
8cf735f84c | ||
|
|
e1db7f3704 | ||
|
|
fd64a068aa | ||
|
|
1945d3dfaa | ||
|
|
42670c7a9f | ||
|
|
81a9ca4254 | ||
|
|
00c0a205d6 | ||
|
|
374d8be96f | ||
|
|
70daff7cec | ||
|
|
22fc14ae50 | ||
|
|
d9736d7d56 | ||
|
|
574763ccfc | ||
|
|
a8cc411945 | ||
|
|
0576548bbe | ||
|
|
0477f8cb03 | ||
|
|
9a2d58faa0 | ||
|
|
b9fd60b395 | ||
|
|
d975f8e64a | ||
|
|
a2bd504e36 | ||
|
|
8a671b9658 | ||
|
|
b7a4f82968 | ||
|
|
0ee121a08f | ||
|
|
708bf4477a | ||
|
|
56a9d9a7f3 | ||
|
|
337fb96e3f | ||
|
|
c5fa53c00f | ||
|
|
e2dc7d24f9 | ||
|
|
d13dd9b3a7 | ||
|
|
aa0f13e348 | ||
|
|
725eab67f9 | ||
|
|
6efa37a14d | ||
|
|
f05ab61421 | ||
|
|
5217ad21ec | ||
|
|
55e570a2b2 | ||
|
|
f64617c88c | ||
|
|
ad4996e553 | ||
|
|
46febea6d3 | ||
|
|
da022a1cf0 | ||
|
|
9d11f2d881 | ||
|
|
d0521b83ae | ||
|
|
d8d6ab96df | ||
|
|
abefe8c21a | ||
|
|
0e0e1ed6fb | ||
|
|
05ec28be48 | ||
|
|
f5e110c212 | ||
|
|
2429d2b89d | ||
|
|
f0c3a568f0 | ||
|
|
a423b41e68 |
185
.github/workflows/00-pr-scanner.yaml
vendored
185
.github/workflows/00-pr-scanner.yaml
vendored
@@ -1,18 +1,19 @@
|
||||
name: 00-pr_scanner
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths-ignore:
|
||||
- '**.yaml'
|
||||
- '**.yml'
|
||||
- '**.md'
|
||||
- '**.sh'
|
||||
- 'website/*'
|
||||
- 'examples/*'
|
||||
- 'docs/*'
|
||||
- 'build/*'
|
||||
- '.github/*'
|
||||
- "**.yaml"
|
||||
- "**.yml"
|
||||
- "**.md"
|
||||
- "**.sh"
|
||||
- "website/*"
|
||||
- "examples/*"
|
||||
- "docs/*"
|
||||
- "build/*"
|
||||
- ".github/*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -22,12 +23,14 @@ jobs:
|
||||
pr-scanner:
|
||||
permissions:
|
||||
actions: read
|
||||
attestations: read
|
||||
checks: read
|
||||
contents: read
|
||||
contents: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
discussions: read
|
||||
models: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
@@ -42,28 +45,144 @@ jobs:
|
||||
GO111MODULE: ""
|
||||
secrets: inherit
|
||||
|
||||
binary-build:
|
||||
if: ${{ github.actor == 'kubescape' }}
|
||||
wf-preparation:
|
||||
name: secret-validator
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
TEST_NAMES: ${{ steps.export_tests_to_env.outputs.TEST_NAMES }}
|
||||
is-secret-set: ${{ steps.check-secret-set.outputs.is-secret-set }}
|
||||
|
||||
steps:
|
||||
- name: check if the necessary secrets are set in github secrets
|
||||
id: check-secret-set
|
||||
env:
|
||||
CUSTOMER: ${{ secrets.CUSTOMER }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: "echo \"is-secret-set=${{ env.CUSTOMER != '' && env.USERNAME != '' && env.PASSWORD != '' && env.CLIENT_ID != '' && env.SECRET_KEY != '' && env.REGISTRY_USERNAME != '' && env.REGISTRY_PASSWORD != '' }}\" >> $GITHUB_OUTPUT\n"
|
||||
|
||||
- id: export_tests_to_env
|
||||
name: set test name
|
||||
run: |
|
||||
echo "TEST_NAMES=$input" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
input: '[
|
||||
"scan_nsa",
|
||||
"scan_mitre",
|
||||
"scan_with_exceptions",
|
||||
"scan_repository",
|
||||
"scan_local_file",
|
||||
"scan_local_glob_files",
|
||||
"scan_local_list_of_files",
|
||||
"scan_git_repository_and_submit_to_backend",
|
||||
"scan_and_submit_to_backend",
|
||||
"scan_customer_configuration",
|
||||
"scan_compliance_score",
|
||||
"scan_custom_framework_scanning_file_scope_testing",
|
||||
"scan_custom_framework_scanning_cluster_scope_testing",
|
||||
"scan_custom_framework_scanning_cluster_and_file_scope_testing"
|
||||
]'
|
||||
|
||||
run-system-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
TEST: ${{ fromJson(needs.wf-preparation.outputs.TEST_NAMES) }}
|
||||
needs: [wf-preparation, pr-scanner]
|
||||
if: ${{ (needs.wf-preparation.outputs.is-secret-set == 'true') && (always() && (contains(needs.*.result, 'success') || contains(needs.*.result, 'skipped')) && !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled'))) }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
uses: ./.github/workflows/b-binary-build-and-e2e-tests.yaml
|
||||
with:
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: ""
|
||||
GO_VERSION: "1.21"
|
||||
RELEASE: "latest"
|
||||
CLIENT: test
|
||||
secrets: inherit
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- uses: anchore/sbom-action/download-syft@v0
|
||||
name: Setup Syft
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
name: Build
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: build --clean --snapshot --single-target
|
||||
env:
|
||||
RELEASE: ""
|
||||
CLIENT: test
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: chmod +x
|
||||
run: chmod +x -R ${PWD}/dist/cli_linux_amd64_v1/kubescape
|
||||
|
||||
- name: Checkout systests repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: armosec/system-tests
|
||||
path: system-tests
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
cache: "pip"
|
||||
|
||||
- name: create env
|
||||
run: ./create_env.sh
|
||||
working-directory: system-tests
|
||||
|
||||
- name: Generate uuid
|
||||
id: uuid
|
||||
run: |
|
||||
echo "RANDOM_UUID=$(uuidgen)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create k8s Kind Cluster
|
||||
id: kind-cluster-install
|
||||
uses: helm/kind-action@v1.10.0
|
||||
with:
|
||||
cluster_name: ${{ steps.uuid.outputs.RANDOM_UUID }}
|
||||
|
||||
- name: run-tests-on-local-built-kubescape
|
||||
env:
|
||||
CUSTOMER: ${{ secrets.CUSTOMER }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
working-directory: system-tests
|
||||
run: |
|
||||
echo "Test history:"
|
||||
echo " ${{ matrix.TEST }} " >/tmp/testhistory
|
||||
cat /tmp/testhistory
|
||||
source systests_python_env/bin/activate
|
||||
|
||||
python3 systest-cli.py \
|
||||
-t ${{ matrix.TEST }} \
|
||||
-b production \
|
||||
-c CyberArmorTests \
|
||||
--duration 3 \
|
||||
--logger DEBUG \
|
||||
--kwargs kubescape=${GITHUB_WORKSPACE}/dist/cli_linux_amd64_v1/kubescape
|
||||
|
||||
deactivate
|
||||
|
||||
- name: Test Report
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: always()
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
report_paths: "system-tests/**/results_xml_format/**.xml"
|
||||
commit: ${{github.event.workflow_run.head_sha}}
|
||||
|
||||
169
.github/workflows/02-release.yaml
vendored
169
.github/workflows/02-release.yaml
vendored
@@ -3,44 +3,16 @@ permissions: read-all
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*-rc.*'
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_publish:
|
||||
description: "Skip publishing artifacts"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
jobs:
|
||||
retag:
|
||||
outputs:
|
||||
NEW_TAG: ${{ steps.tag-calculator.outputs.NEW_TAG }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # ratchet:actions/checkout@v3
|
||||
- id: tag-calculator
|
||||
uses: ./.github/actions/tag-action
|
||||
with:
|
||||
SUB_STRING: "-rc"
|
||||
binary-build:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
needs: [retag]
|
||||
uses: ./.github/workflows/b-binary-build-and-e2e-tests.yaml
|
||||
with:
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: ""
|
||||
GO_VERSION: "1.21"
|
||||
RELEASE: ${{ needs.retag.outputs.NEW_TAG }}
|
||||
CLIENT: release
|
||||
secrets: inherit
|
||||
create-release:
|
||||
release:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
@@ -49,40 +21,99 @@ jobs:
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
statuses: read
|
||||
security-events: read
|
||||
needs: [retag, binary-build]
|
||||
uses: ./.github/workflows/c-create-release.yaml
|
||||
with:
|
||||
RELEASE_NAME: "Release ${{ needs.retag.outputs.NEW_TAG }}"
|
||||
TAG: ${{ needs.retag.outputs.NEW_TAG }}
|
||||
DRAFT: false
|
||||
secrets: inherit
|
||||
publish-image:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: read
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
models: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
uses: ./.github/workflows/d-publish-image.yaml
|
||||
needs: [create-release, retag]
|
||||
with:
|
||||
client: "image-release"
|
||||
image_name: "quay.io/${{ github.repository_owner }}/kubescape-cli"
|
||||
image_tag: ${{ needs.retag.outputs.NEW_TAG }}
|
||||
support_platforms: true
|
||||
cosign: true
|
||||
secrets: inherit
|
||||
security-events: read
|
||||
attestations: read
|
||||
artifact-metadata: read
|
||||
runs-on: ubuntu-large
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install system dependencies for system-tests
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
python3-dev
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
|
||||
- name: Create Cosign Key
|
||||
run: echo "${{ secrets.COSIGN_PRIVATE_KEY_V1 }}" > cosign.key
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
|
||||
|
||||
- uses: anchore/sbom-action/download-syft@v0
|
||||
name: Setup Syft
|
||||
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@v1.10.0
|
||||
with:
|
||||
cluster_name: kubescape-e2e
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean ${{ inputs.skip_publish == true && '--skip=publish' || '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PRIVATE_KEY_V1_PASSWORD }}
|
||||
RELEASE: ${{ github.ref_name }}
|
||||
CLIENT: release
|
||||
RUN_E2E: "true"
|
||||
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
|
||||
CUSTOMER: ${{ secrets.CUSTOMER }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: List collected system-test results (debug)
|
||||
if: always()
|
||||
run: |
|
||||
echo "Listing test-results/system-tests (if any):"
|
||||
ls -laR test-results/system-tests || true
|
||||
|
||||
- name: System Tests Report
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: always()
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
report_paths: "test-results/system-tests/**/results_xml_format/**.xml"
|
||||
annotate_only: true
|
||||
job_summary: true
|
||||
|
||||
42
.github/workflows/03-post-release.yaml
vendored
42
.github/workflows/03-post-release.yaml
vendored
@@ -1,42 +0,0 @@
|
||||
name: 03-post_release
|
||||
permissions: read-all
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
jobs:
|
||||
post_release:
|
||||
name: Post release jobs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Digest
|
||||
uses: MCJack123/ghaction-generate-release-hashes@c03f3111b39432dde3edebe401c5a8d1ffbbf917 # ratchet:MCJack123/ghaction-generate-release-hashes@v1
|
||||
with:
|
||||
hash-type: sha1
|
||||
file-name: kubescape-release-digests
|
||||
- name: Invoke workflow to update packaging
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
if: github.repository_owner == 'kubescape'
|
||||
with:
|
||||
workflow: release.yml
|
||||
repo: kubescape/packaging
|
||||
ref: refs/heads/main
|
||||
token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Invoke workflow to update homebrew tap
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
if: github.repository_owner == 'kubescape'
|
||||
with:
|
||||
workflow: release.yml
|
||||
repo: kubescape/homebrew-tap
|
||||
ref: refs/heads/main
|
||||
token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Invoke workflow to update github action
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
if: github.repository_owner == 'kubescape'
|
||||
with:
|
||||
workflow: release.yaml
|
||||
repo: kubescape/github-action
|
||||
ref: refs/heads/main
|
||||
token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
||||
17
.github/workflows/04-publish-krew-plugin.yaml
vendored
17
.github/workflows/04-publish-krew-plugin.yaml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 04-publish_krew_plugin
|
||||
permissions: read-all
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
jobs:
|
||||
publish_krew_plugin:
|
||||
name: Publish Krew plugin
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'kubescape'
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # ratchet:actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Update new version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@92da038bbf995803124a8e50ebd438b2f37bbbb0 # ratchet:rajatjindal/krew-release-bot@v0.0.43
|
||||
92
.github/workflows/a-pr-scanner.yaml
vendored
92
.github/workflows/a-pr-scanner.yaml
vendored
@@ -27,10 +27,10 @@ jobs:
|
||||
name: Create cross-platform build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-large
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: ${{ inputs.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Test core pkg
|
||||
run: ${{ env.DOCKER_CMD }} go test -v ./...
|
||||
@@ -49,15 +48,15 @@ jobs:
|
||||
run: ${{ env.DOCKER_CMD }} sh -c 'cd httphandler && go test -v ./...'
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
|
||||
- uses: anchore/sbom-action/download-syft@v0.15.2
|
||||
- uses: anchore/sbom-action/download-syft@v0
|
||||
name: Setup Syft
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
name: Build
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean --snapshot
|
||||
args: build --clean --snapshot --single-target
|
||||
env:
|
||||
RELEASE: ${{ inputs.RELEASE }}
|
||||
CLIENT: ${{ inputs.CLIENT }}
|
||||
@@ -67,85 +66,12 @@ jobs:
|
||||
env:
|
||||
RELEASE: ${{ inputs.RELEASE }}
|
||||
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
|
||||
run: ${{ env.DOCKER_CMD }} python3 smoke_testing/init.py ${PWD}/dist/kubescape-ubuntu-latest
|
||||
run: ${{ env.DOCKER_CMD }} python3 smoke_testing/init.py ${PWD}/dist/cli_linux_amd64_v1/kubescape
|
||||
|
||||
- name: golangci-lint
|
||||
continue-on-error: false
|
||||
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # ratchet:golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 10m --build-tags=static
|
||||
version: v2.1
|
||||
args: --timeout 10m
|
||||
only-new-issues: true
|
||||
|
||||
scanners:
|
||||
env:
|
||||
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
name: PR Scanner
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- uses: actions/setup-go@v4
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
- name: Scanning - Forbidden Licenses (go-licenses)
|
||||
id: licenses-scan
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "## Installing go-licenses tool"
|
||||
go install github.com/google/go-licenses@latest
|
||||
echo "## Scanning for forbiden licenses ##"
|
||||
go-licenses check .
|
||||
- name: Scanning - Credentials (GitGuardian)
|
||||
if: ${{ env.GITGUARDIAN_API_KEY }}
|
||||
continue-on-error: true
|
||||
id: credentials-scan
|
||||
uses: GitGuardian/ggshield-action@4ab2994172fadab959240525e6b833d9ae3aca61 # ratchet:GitGuardian/ggshield-action@master
|
||||
with:
|
||||
args: -v --all-policies
|
||||
env:
|
||||
GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||
GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
|
||||
GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
|
||||
- name: Scanning - Vulnerabilities (Snyk)
|
||||
if: ${{ env.SNYK_TOKEN }}
|
||||
id: vulnerabilities-scan
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/golang@806182742461562b67788a64410098c9d9b96adb # ratchet:snyk/actions/golang@master
|
||||
with:
|
||||
command: test --all-projects
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
|
||||
- name: Test coverage
|
||||
id: unit-test
|
||||
run: go test -v ${{ inputs.UNIT_TESTS_PATH }} -covermode=count -coverprofile=coverage.out
|
||||
|
||||
- name: Convert coverage count to lcov format
|
||||
uses: jandelgado/gcov2lcov-action@v1
|
||||
|
||||
- name: Submit coverage tests to Coveralls
|
||||
continue-on-error: true
|
||||
uses: coverallsapp/github-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
|
||||
path-to-lcov: coverage.lcov
|
||||
|
||||
- name: Comment results to PR
|
||||
continue-on-error: true # Warning: This might break opening PRs from forks
|
||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # ratchet:peter-evans/create-or-update-comment@v2.1.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
Scan results:
|
||||
- License scan: ${{ steps.licenses-scan.outcome }}
|
||||
- Credentials scan: ${{ steps.credentials-scan.outcome }}
|
||||
- Vulnerabilities scan: ${{ steps.vulnerabilities-scan.outcome }}
|
||||
reactions: 'eyes'
|
||||
|
||||
271
.github/workflows/b-binary-build-and-e2e-tests.yaml
vendored
271
.github/workflows/b-binary-build-and-e2e-tests.yaml
vendored
@@ -1,271 +0,0 @@
|
||||
name: b-binary-build-and-e2e-tests
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
COMPONENT_NAME:
|
||||
required: false
|
||||
type: string
|
||||
default: "kubescape"
|
||||
RELEASE:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
CLIENT:
|
||||
required: false
|
||||
type: string
|
||||
default: "test"
|
||||
GO_VERSION:
|
||||
required: false
|
||||
type: string
|
||||
default: "1.21"
|
||||
GO111MODULE:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
CGO_ENABLED:
|
||||
type: number
|
||||
default: 1
|
||||
required: false
|
||||
BINARY_TESTS:
|
||||
type: string
|
||||
required: false
|
||||
default: '[ "scan_nsa", "scan_mitre", "scan_with_exceptions", "scan_repository", "scan_local_file", "scan_local_glob_files", "scan_local_list_of_files", "scan_nsa_and_submit_to_backend", "scan_mitre_and_submit_to_backend", "scan_local_repository_and_submit_to_backend", "scan_repository_from_url_and_submit_to_backend", "scan_with_exception_to_backend", "scan_with_custom_framework", "scan_customer_configuration", "host_scanner", "scan_compliance_score", "control_cluster_from_CLI_config_scan_exclude_namespaces", "control_cluster_from_CLI_config_scan_include_namespaces", "control_cluster_from_CLI_config_scan_host_scanner_enabled", "control_cluster_from_CLI_config_scan_MITRE_framework", "control_cluster_from_CLI_vulnerabilities_scan_default", "control_cluster_from_CLI_vulnerabilities_scan_include_namespaces" ]'
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
COMPONENT_NAME:
|
||||
required: true
|
||||
type: string
|
||||
RELEASE:
|
||||
required: true
|
||||
type: string
|
||||
CLIENT:
|
||||
required: true
|
||||
type: string
|
||||
GO_VERSION:
|
||||
type: string
|
||||
default: "1.21"
|
||||
GO111MODULE:
|
||||
required: true
|
||||
type: string
|
||||
CGO_ENABLED:
|
||||
type: number
|
||||
default: 1
|
||||
BINARY_TESTS:
|
||||
type: string
|
||||
default: '[ "scan_nsa", "scan_mitre", "scan_with_exceptions", "scan_repository", "scan_local_file", "scan_local_glob_files", "scan_local_list_of_files", "scan_nsa_and_submit_to_backend", "scan_mitre_and_submit_to_backend", "scan_local_repository_and_submit_to_backend", "scan_repository_from_url_and_submit_to_backend", "scan_with_exception_to_backend", "scan_with_custom_framework", "scan_customer_configuration", "host_scanner", "scan_compliance_score", "scan_custom_framework_scanning_file_scope_testing", "scan_custom_framework_scanning_cluster_scope_testing", "scan_custom_framework_scanning_cluster_and_file_scope_testing" ]'
|
||||
|
||||
jobs:
|
||||
wf-preparation:
|
||||
name: secret-validator
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
TEST_NAMES: ${{ steps.export_tests_to_env.outputs.TEST_NAMES }}
|
||||
is-secret-set: ${{ steps.check-secret-set.outputs.is-secret-set }}
|
||||
|
||||
steps:
|
||||
- name: check if the necessary secrets are set in github secrets
|
||||
id: check-secret-set
|
||||
env:
|
||||
CUSTOMER: ${{ secrets.CUSTOMER }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: "echo \"is-secret-set=${{ env.CUSTOMER != '' && \n env.USERNAME != '' &&\n env.PASSWORD != '' &&\n env.CLIENT_ID != '' &&\n env.SECRET_KEY != '' &&\n env.REGISTRY_USERNAME != '' &&\n env.REGISTRY_PASSWORD != ''\n }}\" >> $GITHUB_OUTPUT\n"
|
||||
|
||||
- id: export_tests_to_env
|
||||
name: set test name
|
||||
run: |
|
||||
echo "TEST_NAMES=$input" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
input: ${{ inputs.BINARY_TESTS }}
|
||||
|
||||
check-secret:
|
||||
name: check if QUAYIO_REGISTRY_USERNAME & QUAYIO_REGISTRY_PASSWORD is set in github secrets
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is-secret-set: ${{ steps.check-secret-set.outputs.is-secret-set }}
|
||||
steps:
|
||||
- name: check if QUAYIO_REGISTRY_USERNAME & QUAYIO_REGISTRY_PASSWORD is set in github secrets
|
||||
id: check-secret-set
|
||||
env:
|
||||
QUAYIO_REGISTRY_USERNAME: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
|
||||
QUAYIO_REGISTRY_PASSWORD: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
echo "is-secret-set=${{ env.QUAYIO_REGISTRY_USERNAME != '' && env.QUAYIO_REGISTRY_PASSWORD != '' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
binary-build:
|
||||
name: Create cross-platform build
|
||||
needs: wf-preparation
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: ${{ inputs.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Test core pkg
|
||||
run: ${{ env.DOCKER_CMD }} go test -v ./...
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
|
||||
- name: Test httphandler pkg
|
||||
run: ${{ env.DOCKER_CMD }} sh -c 'cd httphandler && go test -v ./...'
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
|
||||
- uses: anchore/sbom-action/download-syft@v0.15.2
|
||||
name: Setup Syft
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
name: Build
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean --snapshot
|
||||
env:
|
||||
RELEASE: ${{ inputs.RELEASE }}
|
||||
CLIENT: ${{ inputs.CLIENT }}
|
||||
CGO_ENABLED: ${{ inputs.CGO_ENABLED }}
|
||||
|
||||
- name: Smoke Testing
|
||||
env:
|
||||
RELEASE: ${{ inputs.RELEASE }}
|
||||
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
|
||||
run: ${{ env.DOCKER_CMD }} python3 smoke_testing/init.py ${PWD}/dist/kubescape-ubuntu-latest
|
||||
|
||||
- name: golangci-lint
|
||||
continue-on-error: true
|
||||
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # ratchet:golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 10m --build-tags=static
|
||||
only-new-issues: true
|
||||
|
||||
- uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # ratchet:actions/upload-artifact@v3.1.1
|
||||
name: Upload artifacts
|
||||
with:
|
||||
name: kubescape
|
||||
path: dist/kubescape*
|
||||
if-no-files-found: error
|
||||
|
||||
build-http-image:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
pull-requests: read
|
||||
needs: [check-secret]
|
||||
uses: kubescape/workflows/.github/workflows/incluster-comp-pr-merged.yaml@main
|
||||
with:
|
||||
IMAGE_NAME: quay.io/${{ github.repository_owner }}/kubescape
|
||||
IMAGE_TAG: ${{ inputs.RELEASE }}
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: "on"
|
||||
BUILD_PLATFORM: linux/amd64,linux/arm64
|
||||
GO_VERSION: "1.21"
|
||||
REQUIRED_TESTS: '[
|
||||
"ks_microservice_create_2_cronjob_mitre_and_nsa_proxy",
|
||||
"ks_microservice_triggering_with_cron_job",
|
||||
"ks_microservice_update_cronjob_schedule",
|
||||
"ks_microservice_delete_cronjob",
|
||||
"ks_microservice_create_2_cronjob_mitre_and_nsa",
|
||||
"ks_microservice_ns_creation",
|
||||
"ks_microservice_on_demand",
|
||||
"ks_microservice_mitre_framework_on_demand",
|
||||
"ks_microservice_nsa_and_mitre_framework_demand",
|
||||
"scan_compliance_score"
|
||||
]'
|
||||
COSIGN: true
|
||||
HELM_E2E_TEST: true
|
||||
FORCE: true
|
||||
secrets: inherit
|
||||
|
||||
run-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
TEST: ${{ fromJson(needs.wf-preparation.outputs.TEST_NAMES) }}
|
||||
needs: [wf-preparation, binary-build]
|
||||
if: ${{ (needs.wf-preparation.outputs.is-secret-set == 'true') && (always() && (contains(needs.*.result, 'success') || contains(needs.*.result, 'skipped')) && !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled'))) }}
|
||||
runs-on: ubuntu-latest # This cannot change
|
||||
steps:
|
||||
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
|
||||
id: download-artifact
|
||||
with:
|
||||
name: kubescape
|
||||
path: "~"
|
||||
|
||||
- run: ls -laR
|
||||
|
||||
- name: chmod +x
|
||||
run: chmod +x -R ${{steps.download-artifact.outputs.download-path}}/kubescape-ubuntu-latest
|
||||
|
||||
- name: Checkout systests repo
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # ratchet:actions/checkout@v3
|
||||
with:
|
||||
repository: armosec/system-tests
|
||||
path: .
|
||||
|
||||
- uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # ratchet:actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8.13'
|
||||
cache: 'pip'
|
||||
|
||||
- name: create env
|
||||
run: ./create_env.sh
|
||||
|
||||
- name: Generate uuid
|
||||
id: uuid
|
||||
run: |
|
||||
echo "RANDOM_UUID=$(uuidgen)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create k8s Kind Cluster
|
||||
id: kind-cluster-install
|
||||
uses: helm/kind-action@d08cf6ff1575077dee99962540d77ce91c62387d # ratchet:helm/kind-action@v1.3.0
|
||||
with:
|
||||
cluster_name: ${{ steps.uuid.outputs.RANDOM_UUID }}
|
||||
|
||||
- name: run-tests-on-local-built-kubescape
|
||||
env:
|
||||
CUSTOMER: ${{ secrets.CUSTOMER }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.PASSWORD }}
|
||||
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
echo "Test history:"
|
||||
echo " ${{ matrix.TEST }} " >/tmp/testhistory
|
||||
cat /tmp/testhistory
|
||||
source systests_python_env/bin/activate
|
||||
|
||||
python3 systest-cli.py \
|
||||
-t ${{ matrix.TEST }} \
|
||||
-b production \
|
||||
-c CyberArmorTests \
|
||||
--duration 3 \
|
||||
--logger DEBUG \
|
||||
--kwargs kubescape=${{steps.download-artifact.outputs.download-path}}/kubescape-ubuntu-latest
|
||||
|
||||
deactivate
|
||||
|
||||
- name: Test Report
|
||||
uses: mikepenz/action-junit-report@6e9933f4a97f4d2b99acef4d7b97924466037882 # ratchet:mikepenz/action-junit-report@v3.6.1
|
||||
if: always() # always run even if the previous step fails
|
||||
with:
|
||||
report_paths: '**/results_xml_format/**.xml'
|
||||
commit: ${{github.event.workflow_run.head_sha}}
|
||||
41
.github/workflows/build-image.yaml
vendored
41
.github/workflows/build-image.yaml
vendored
@@ -1,41 +0,0 @@
|
||||
name: build-image
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
CLIENT:
|
||||
required: false
|
||||
type: string
|
||||
default: "test"
|
||||
IMAGE_TAG:
|
||||
required: true
|
||||
type: string
|
||||
CO_SIGN:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
PLATFORMS:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
jobs:
|
||||
build-http-image:
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: kubescape/workflows/.github/workflows/incluster-comp-pr-merged.yaml@main
|
||||
with:
|
||||
IMAGE_NAME: quay.io/${{ github.repository_owner }}/kubescape
|
||||
IMAGE_TAG: ${{ inputs.IMAGE_TAG }}
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: "on"
|
||||
BUILD_PLATFORM: ${{ inputs.PLATFORMS && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
|
||||
GO_VERSION: "1.21"
|
||||
REQUIRED_TESTS: '[]'
|
||||
COSIGN: ${{ inputs.CO_SIGN }}
|
||||
HELM_E2E_TEST: false
|
||||
FORCE: true
|
||||
secrets: inherit
|
||||
82
.github/workflows/c-create-release.yaml
vendored
82
.github/workflows/c-create-release.yaml
vendored
@@ -1,82 +0,0 @@
|
||||
name: c-create_release
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
RELEASE_NAME:
|
||||
description: 'Release name'
|
||||
required: true
|
||||
type: string
|
||||
TAG:
|
||||
description: 'Tag name'
|
||||
required: true
|
||||
type: string
|
||||
DRAFT:
|
||||
description: 'Create draft release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
jobs:
|
||||
create-release:
|
||||
name: create-release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MAC_OS: macos-latest
|
||||
UBUNTU_OS: ubuntu-latest
|
||||
WINDOWS_OS: windows-latest
|
||||
# permissions:
|
||||
# contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
|
||||
id: download-artifact
|
||||
with:
|
||||
path: .
|
||||
|
||||
# TODO: kubescape-windows-latest is deprecated and should be removed
|
||||
- name: Get kubescape.exe from kubescape-windows-latest.exe
|
||||
run: cp ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-${{ env.WINDOWS_OS }}.exe ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape.exe
|
||||
|
||||
- name: Set release token
|
||||
id: set-token
|
||||
run: |
|
||||
if [ "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" != "" ]; then
|
||||
echo "token=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "token=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
- name: List artifacts
|
||||
run: |
|
||||
find . -type f -print
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
token: ${{ steps.set-token.outputs.token }}
|
||||
name: ${{ inputs.RELEASE_NAME }}
|
||||
tag_name: ${{ inputs.TAG }}
|
||||
body: ${{ github.event.pull_request.body }}
|
||||
draft: ${{ inputs.DRAFT }}
|
||||
prerelease: false
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
./kubescape/kubescape-${{ env.UBUNTU_OS }}.tar.gz
|
||||
./kubescape/kubescape-${{ env.UBUNTU_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape-${{ env.WINDOWS_OS }}.exe
|
||||
./kubescape/kubescape-${{ env.WINDOWS_OS }}.tar.gz
|
||||
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.tar.gz
|
||||
./kubescape/kubescape-arm64-${{ env.MAC_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}
|
||||
./kubescape/kubescape-${{ env.UBUNTU_OS }}
|
||||
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}.tar.gz
|
||||
./kubescape/kubescape-arm64-${{ env.MAC_OS }}
|
||||
./kubescape/kubescape-${{ env.MAC_OS }}.tar.gz
|
||||
./kubescape/kubescape-${{ env.MAC_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape.exe
|
||||
./kubescape/kubescape-${{ env.WINDOWS_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}.tar.gz.sbom
|
||||
./kubescape/kubescape-${{ env.MAC_OS }}
|
||||
./kubescape/kubescape-arm64-${{ env.MAC_OS }}.tar.gz
|
||||
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.exe
|
||||
|
||||
96
.github/workflows/d-publish-image.yaml
vendored
96
.github/workflows/d-publish-image.yaml
vendored
@@ -1,96 +0,0 @@
|
||||
name: d-publish-image
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
client:
|
||||
description: 'client name'
|
||||
required: true
|
||||
type: string
|
||||
image_tag:
|
||||
description: 'image tag'
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
description: 'image registry and name'
|
||||
required: true
|
||||
type: string
|
||||
cosign:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
description: 'run cosign on released image'
|
||||
support_platforms:
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
description: 'support amd64/arm64'
|
||||
jobs:
|
||||
check-secret:
|
||||
name: check if QUAYIO_REGISTRY_USERNAME & QUAYIO_REGISTRY_PASSWORD is set in github secrets
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is-secret-set: ${{ steps.check-secret-set.outputs.is-secret-set }}
|
||||
steps:
|
||||
- name: check if QUAYIO_REGISTRY_USERNAME & QUAYIO_REGISTRY_PASSWORD is set in github secrets
|
||||
id: check-secret-set
|
||||
env:
|
||||
QUAYIO_REGISTRY_USERNAME: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
|
||||
QUAYIO_REGISTRY_PASSWORD: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
echo "is-secret-set=${{ env.QUAYIO_REGISTRY_USERNAME != '' && env.QUAYIO_REGISTRY_PASSWORD != '' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
build-cli-image:
|
||||
needs: [check-secret]
|
||||
if: needs.check-secret.outputs.is-secret-set == 'true'
|
||||
name: Build image and upload to registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # ratchet:actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # ratchet:docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f03ac48505955848960e80bbb68046aa35c7b9e7 # ratchet:docker/setup-buildx-action@v2
|
||||
- name: Login to Quay.io
|
||||
env:
|
||||
QUAY_PASSWORD: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
|
||||
QUAY_USERNAME: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
|
||||
run: docker login -u="${QUAY_USERNAME}" -p="${QUAY_PASSWORD}" quay.io
|
||||
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
|
||||
id: download-artifact
|
||||
with:
|
||||
path: .
|
||||
- name: mv kubescape amd64 binary
|
||||
run: mv ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-ubuntu-latest kubescape-amd64-ubuntu-latest
|
||||
- name: mv kubescape arm64 binary
|
||||
run: mv ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-arm64-ubuntu-latest kubescape-arm64-ubuntu-latest
|
||||
- name: chmod +x
|
||||
run: chmod +x -v kubescape-a*
|
||||
- name: Build and push images
|
||||
run: docker buildx build . --file build/kubescape-cli.Dockerfile --tag ${{ inputs.image_name }}:${{ inputs.image_tag }} --tag ${{ inputs.image_name }}:latest --build-arg image_version=${{ inputs.image_tag }} --build-arg client=${{ inputs.client }} --push --platform linux/amd64,linux/arm64
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@main
|
||||
with:
|
||||
cosign-release: 'v2.2.2'
|
||||
- name: sign kubescape container image
|
||||
if: ${{ inputs.cosign }}
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY_V1 }}
|
||||
COSIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.COSIGN_PRIVATE_KEY_V1_PASSWORD }}
|
||||
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY_V1 }}
|
||||
run: |
|
||||
# Sign the image with keyless mode
|
||||
cosign sign -y ${{ inputs.image_name }}:${{ inputs.image_tag }}
|
||||
|
||||
# Sign the image with key for verifier clients without keyless support
|
||||
# Put the key from environment variable to a file
|
||||
echo "$COSIGN_PRIVATE_KEY" > cosign.key
|
||||
printf "$COSIGN_PRIVATE_KEY_PASSWORD" | cosign sign -key cosign.key -y ${{ inputs.image_name }}:${{ inputs.image_tag }}
|
||||
rm cosign.key
|
||||
# Verify the image
|
||||
echo "$COSIGN_PUBLIC_KEY" > cosign.pub
|
||||
cosign verify -key cosign.pub ${{ inputs.image_name }}:${{ inputs.image_tag }}
|
||||
|
||||
8
.github/workflows/scorecard.yml
vendored
8
.github/workflows/scorecard.yml
vendored
@@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
|
||||
uses: ossf/scorecard-action@v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -67,6 +67,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
20
.github/workflows/z-close-typos-issues.yaml
vendored
20
.github/workflows/z-close-typos-issues.yaml
vendored
@@ -1,20 +0,0 @@
|
||||
permissions: read-all
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
jobs:
|
||||
open_PR_message:
|
||||
if: github.event.label.name == 'typo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ben-z/actions-comment-on-issue@10be23f9c43ac792663043420fda29dde07e2f0f # ratchet:ben-z/actions-comment-on-issue@1.0.2
|
||||
with:
|
||||
message: "Hello! :wave:\n\nThis issue is being automatically closed, Please open a PR with a relevant fix."
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto_close_issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lee-dohm/close-matching-issues@e9e43aad2fa6f06a058cedfd8fb975fd93b56d8f # ratchet:lee-dohm/close-matching-issues@v2
|
||||
with:
|
||||
query: 'label:typo'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,5 +9,10 @@
|
||||
ca.srl
|
||||
*.out
|
||||
ks
|
||||
cosign.key
|
||||
|
||||
dist/
|
||||
|
||||
# Test output files
|
||||
customFilename.pdf
|
||||
customFilename.xml
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
dupl:
|
||||
threshold: 200
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 2
|
||||
gocognit:
|
||||
min-complexity: 65
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- gosec
|
||||
- staticcheck
|
||||
- nolintlint
|
||||
- gofmt
|
||||
- unused
|
||||
- govet
|
||||
- bodyclose
|
||||
- typecheck
|
||||
- goimports
|
||||
- ineffassign
|
||||
- gosimple
|
||||
- gosec
|
||||
- nolintlint
|
||||
disable:
|
||||
# temporarily disabled
|
||||
- varcheck
|
||||
- errcheck
|
||||
- dupl
|
||||
- gocritic
|
||||
- errcheck
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- gocritic
|
||||
- lll
|
||||
- nakedret
|
||||
- revive
|
||||
- stylecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
#- forbidigo # <- see later
|
||||
# should remain disabled
|
||||
- deadcode # deprecated linter
|
||||
- maligned
|
||||
- lll
|
||||
- gochecknoinits
|
||||
- gochecknoglobals
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- revive
|
||||
text: "var-naming"
|
||||
- linters:
|
||||
- revive
|
||||
text: "type name will be used as (.+?) by other packages, and that stutters"
|
||||
- linters:
|
||||
- stylecheck
|
||||
text: "ST1003"
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 200
|
||||
gocognit:
|
||||
min-complexity: 65
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 2
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- revive
|
||||
text: var-naming
|
||||
- linters:
|
||||
- revive
|
||||
text: type name will be used as (.+?) by other packages, and that stutters
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: ST1003
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
136
.goreleaser.yaml
136
.goreleaser.yaml
@@ -1,41 +1,119 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines bellow are called `modelines`. See `:help modeline`
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
- go test -v ./...
|
||||
- go -C httphandler test -v ./...
|
||||
|
||||
archives:
|
||||
- id: cli
|
||||
ids:
|
||||
- cli
|
||||
formats:
|
||||
- binary
|
||||
- tar.gz
|
||||
|
||||
builds:
|
||||
- id: "kubescape-cli"
|
||||
- id: cli
|
||||
binary: kubescape
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X "github.com/kubescape/kubescape/v3/core/cautils.BuildNumber={{.Env.RELEASE}}"
|
||||
binary: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- if eq .Arch "amd64" }}
|
||||
{{- else }}{{ .Arch }}-{{ end }}
|
||||
{{- if eq .Os "darwin" }}macos
|
||||
{{- else if eq .Os "linux" }}ubuntu
|
||||
{{- else }}{{ .Os }}{{ end }}-latest
|
||||
no_unique_dist_dir: true
|
||||
- -s -w
|
||||
- -X "github.com/kubescape/kubescape/v3/core/cautils.BuildNumber={{.Env.RELEASE}}"
|
||||
- -X "github.com/kubescape/kubescape/v3/core/cautils.Client={{.Env.CLIENT}}"
|
||||
hooks:
|
||||
post:
|
||||
- cmd: >
|
||||
{{ if eq .Arch "amd64" }}
|
||||
/bin/sh -lc 'sh build/goreleaser-post-e2e.sh'
|
||||
{{ end }}
|
||||
- id: downloader
|
||||
dir: downloader
|
||||
binary: downloader
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- id: http
|
||||
dir: httphandler
|
||||
binary: ksserver
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .Binary }}
|
||||
nfpms:
|
||||
- id: cli
|
||||
package_name: kubescape
|
||||
ids:
|
||||
- cli
|
||||
vendor: Kubescape
|
||||
homepage: https://kubescape.io/
|
||||
maintainer: matthiasb@kubescape.io
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
bindir: /usr/bin
|
||||
|
||||
docker_signs:
|
||||
- stdin: "{{ .Env.COSIGN_PWD }}"
|
||||
|
||||
dockers_v2:
|
||||
- id: cli
|
||||
images:
|
||||
- "quay.io/kubescape/kubescape-cli"
|
||||
tags:
|
||||
- "{{ .Tag }}"
|
||||
labels:
|
||||
"org.opencontainers.image.description": "Kubescape CLI"
|
||||
"org.opencontainers.image.created": "{{.Date}}"
|
||||
"org.opencontainers.image.name": "{{.ProjectName}}"
|
||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||
"org.opencontainers.image.version": "{{.Version}}"
|
||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||
ids:
|
||||
- cli
|
||||
dockerfile: build/kubescape-cli.Dockerfile
|
||||
- id: http
|
||||
images:
|
||||
- "quay.io/kubescape/kubescape"
|
||||
tags:
|
||||
- "{{ .Tag }}"
|
||||
labels:
|
||||
"org.opencontainers.image.description": "Kubescape microservice"
|
||||
"org.opencontainers.image.created": "{{.Date}}"
|
||||
"org.opencontainers.image.name": "{{.ProjectName}}"
|
||||
"org.opencontainers.image.revision": "{{.FullCommit}}"
|
||||
"org.opencontainers.image.version": "{{.Version}}"
|
||||
"org.opencontainers.image.source": "{{.GitURL}}"
|
||||
ids:
|
||||
- downloader
|
||||
- http
|
||||
dockerfile: build/Dockerfile
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
@@ -44,5 +122,25 @@ changelog:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.sha256"
|
||||
|
||||
sboms:
|
||||
- artifacts: archive
|
||||
- artifacts: binary
|
||||
|
||||
krews:
|
||||
- name: kubescape
|
||||
ids:
|
||||
- cli
|
||||
skip_upload: true
|
||||
homepage: https://kubescape.io/
|
||||
description: It includes risk analysis, security compliance, and misconfiguration scanning with an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities.
|
||||
short_description: Scan resources and cluster configs against security frameworks.
|
||||
|
||||
release:
|
||||
draft: false
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||
|
||||
42
.krew.yaml
42
.krew.yaml
@@ -1,42 +0,0 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: kubescape
|
||||
spec:
|
||||
homepage: https://github.com/kubescape/kubescape/
|
||||
shortDescription: Scan resources and cluster configs against security frameworks.
|
||||
version: {{ .TagName }}
|
||||
description: |
|
||||
It includes risk analysis, security compliance, and misconfiguration scanning
|
||||
with an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities.
|
||||
platforms:
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-macos-latest.tar.gz" .TagName }}
|
||||
bin: kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-arm64-macos-latest.tar.gz" .TagName }}
|
||||
bin: kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-ubuntu-latest.tar.gz" .TagName }}
|
||||
bin: kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-arm64-ubuntu-latest.tar.gz" .TagName }}
|
||||
bin: kubescape
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-windows-latest.tar.gz" .TagName }}
|
||||
bin: kubescape.exe
|
||||
21
ADOPTERS.md
21
ADOPTERS.md
@@ -1,22 +1,5 @@
|
||||
# Adopters
|
||||
|
||||
# Well-known companies
|
||||
Well-known companies who are using and/or contributing to Kubescape are (in alphabetical order):
|
||||
* Accenture
|
||||
* Amazon.com
|
||||
* IBM
|
||||
* Intel
|
||||
* Meetup
|
||||
* RedHat
|
||||
* Scaleway
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
# Users
|
||||
|
||||
If you want to be listed here and share with others your experience, open a PR and add the bellow table:
|
||||
|
||||
|
||||
| Name | Company | Use case | Contact for questions (optional) |
|
||||
| ---- | ------- | -------- | -------------------------------- |
|
||||
| Yonathan Amzallag | ARMO | Vulnerability monitoring | yonatana@armosec.io |
|
||||
| Engin Diri | Schwarz IT (SIT) | Ensure continuous compliance for edge k8s cluster | engin.diri@mail.schwarz |
|
||||
| Idan Bidani | Cox Communications | Security analysis for k8s best practices in CI pipelines of 3,000 applications 🔒☸ | idan.bidani@cox.com |
|
||||
Go to the [centralized ADOPTERS.md](https://github.com/kubescape/project-governance/blob/main/ADOPTERS.md)
|
||||
@@ -1,3 +1,5 @@
|
||||
## Code of Conduct
|
||||
# Code of Conduct
|
||||
|
||||
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
Go to the [centralized CODE_OF_CONDUCT.md](https://github.com/kubescape/project-governance/blob/main/CODE_OF_CONDUCT.md)
|
||||
|
||||
5
COMMUNITY.md
Normal file
5
COMMUNITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Community
|
||||
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
Go to the [centralized COMMUNITY.md](https://github.com/kubescape/project-governance/blob/main/COMMUNITY.md)
|
||||
@@ -1,98 +1,5 @@
|
||||
# Contributing
|
||||
|
||||
First, it is awesome that you are considering contributing to Kubescape! Contributing is important and fun and we welcome your efforts.
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
When contributing, we categorize contributions into two:
|
||||
* Small code changes or fixes, whose scope is limited to a single or two files
|
||||
* Complex features and improvements, with potentially unlimited scope
|
||||
|
||||
If you have a small change, feel free to fire up a Pull Request.
|
||||
|
||||
When planning a bigger change, please first discuss the change you wish to make via an issue,
|
||||
so the maintainers are able to help guide you and let you know if you are going in the right direction.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please follow our [code of conduct](CODE_OF_CONDUCT.md) in all of your interactions within the project.
|
||||
|
||||
## Build and test locally
|
||||
|
||||
Please follow the [instructions here](https://github.com/kubescape/kubescape/wiki/Building).
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||
build.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Open Pull Request to the `master` branch.
|
||||
4. We will merge the Pull Request once you have the sign-off.
|
||||
|
||||
## Developer Certificate of Origin
|
||||
|
||||
All commits to the project must be "signed off", which states that you agree to the terms of the [Developer Certificate of Origin](https://developercertificate.org/). This is done by adding a "Signed-off-by:" line in the commit message, with your name and email address.
|
||||
|
||||
Commits made through the GitHub web application are automatically signed off.
|
||||
|
||||
### Configuring Git to sign off commits
|
||||
|
||||
First, configure your name and email address in Git global settings:
|
||||
|
||||
```
|
||||
$ git config --global user.name "John Doe"
|
||||
$ git config --global user.email johndoe@example.com
|
||||
```
|
||||
|
||||
You can now sign off per-commit, or configure Git to always sign off commits per repository.
|
||||
|
||||
### Sign off per-commit
|
||||
|
||||
Add [`-s`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s) to your Git command line. For example:
|
||||
|
||||
```git commit -s -m "Fix issue 64738"```
|
||||
|
||||
This is tedious, and if you forget, you'll have to [amend your commit](#fixing-a-commit-where-the-dco-failed).
|
||||
|
||||
### Configure a repository to always include sign off
|
||||
|
||||
There are many ways to achieve this with Git hooks, but the simplest is to do the following:
|
||||
|
||||
```
|
||||
cd your-repo
|
||||
curl -Ls https://gist.githubusercontent.com/dixudx/7d7edea35b4d91e1a2a8fbf41d0954fa/raw/prepare-commit-msg -o .git/hooks/prepare-commit-msg
|
||||
chmod +x .git/hooks/prepare-commit-msg
|
||||
```
|
||||
|
||||
### Use semantic commit messages (optional)
|
||||
|
||||
When contributing, you could consider using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), in order to improve logs readability and help us to automatically generate `CHANGELOG`s.
|
||||
|
||||
Format: `<type>(<scope>): <subject>`
|
||||
|
||||
`<scope>` is optional
|
||||
|
||||
#### Example
|
||||
|
||||
```
|
||||
feat(cmd): add kubectl plugin
|
||||
^--^ ^-^ ^----------------^
|
||||
| | |
|
||||
| | +-> subject: summary in present tense.
|
||||
| |
|
||||
| +-------> scope: point of interest
|
||||
|
|
||||
+-------> type: chore, docs, feat, fix, refactor, style, or test.
|
||||
```
|
||||
|
||||
More Examples:
|
||||
* `feat`: new feature for the user, not a new feature for build script
|
||||
* `fix`: bug fix for the user, not a fix to a build script
|
||||
* `docs`: changes to the documentation
|
||||
* `style`: formatting, missing semi colons, etc; no production code change
|
||||
* `refactor`: refactoring production code, eg. renaming a variable
|
||||
* `test`: adding missing tests, refactoring tests; no production code change
|
||||
* `chore`: updating grunt tasks etc; no production code change
|
||||
|
||||
## Fixing a commit where the DCO failed
|
||||
|
||||
Check out [this guide](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md).
|
||||
Go to the [centralized CONTRIBUTING.md](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md)
|
||||
|
||||
@@ -1,65 +1,5 @@
|
||||
# Governance of Kubescape
|
||||
# Governance
|
||||
|
||||
## Overview
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
The Kubescape project is an open-source initiative dedicated to improve security and best practices in Kubernetes environments. This document outlines the governance structure of the Kubescape project and provides guidance for its community contributors.
|
||||
|
||||
## Decision Making
|
||||
|
||||
### Maintainers
|
||||
|
||||
- Maintainers are responsible for the smooth operation of the project.
|
||||
- They review and merge pull requests, manage releases, and ensure the quality and stability of the codebase.
|
||||
- Maintainers are chosen based on their ongoing contributions and their demonstrated commitment to the project.
|
||||
- Everyone who had at least 5 code contribution in the last 12 month can submit her/himself for joining the maintainer team
|
||||
- Maintainers who are not taken part in the project work (code, reviews, discussions) for 12 month are automaticaly removed from the maintainer team
|
||||
|
||||
|
||||
### Committers
|
||||
|
||||
- Committers are contributors who have made significant and consistent contributions to the project.
|
||||
- They have the ability to merge minor pull requests if assigned by maintainers.
|
||||
- A contributor can be proposed as a committer by any existing maintainer. The proposal will be reviewed and voted on by the existing maintainers.
|
||||
|
||||
### Community Members
|
||||
|
||||
- Anyone can become a community member by contributing to the project. This can be in the form of code contributions, documentation, or any other form of project support.
|
||||
|
||||
## Processes
|
||||
|
||||
### Proposing Changes
|
||||
|
||||
1. Open an issue on the project repository to discuss the proposed change.
|
||||
2. Once there is consensus around the proposed change, create a pull request.
|
||||
3. Pull requests will be reviewed by committers and/or maintainers.
|
||||
4. Once the pull request has received approval, it can be merged into the main codebase.
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
1. In case of any conflicts, it is primarily the responsibility of the parties involved to resolve it.
|
||||
2. If the conflict cannot be resolved, it will be escalated to the maintainers for resolution.
|
||||
3. Maintainers' decision will be final in case of unresolved conflicts.
|
||||
|
||||
## Roles and Responsibilities
|
||||
|
||||
### Maintainers
|
||||
|
||||
- Ensure the quality and stability of the project.
|
||||
- Resolve conflicts.
|
||||
- Provide direction and set priorities for the project.
|
||||
|
||||
### Committers
|
||||
|
||||
- Review and merge minor pull requests.
|
||||
- Assist maintainers in project tasks.
|
||||
- Promote best practices within the community.
|
||||
|
||||
### Community Members
|
||||
|
||||
- Contribute to the project in any form.
|
||||
- Participate in discussions and provide feedback.
|
||||
- Respect the code of conduct and governance of the project.
|
||||
|
||||
## Changes to the Governance Document
|
||||
|
||||
Proposed changes to this governance document should follow the same process as any other code change to the Kubescape project (see "Proposing Changes").
|
||||
Go to the [centralized GOVERNANCE.md](https://github.com/kubescape/project-governance/blob/main/GOVERNANCE.md)
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# Maintainers
|
||||
|
||||
The following table lists the Kubescape project core maintainers:
|
||||
|
||||
| Name | GitHub | Organization | Added/Renewed On |
|
||||
| --- | --- | --- | --- |
|
||||
| [Matthias Bertschy](https://www.linkedin.com/in/matthias-bertschy-b427b815/) | [@matthyx](https://github.com/matthyx) | [ARMO](https://www.armosec.io/) | 2023-01-01 |
|
||||
| [Craig Box](https://www.linkedin.com/in/crbnz/) | [@craigbox](https://github.com/craigbox) | [Solo.io](https://www.solo.io/) | 2022-10-31 |
|
||||
| [Ben Hirschberg](https://www.linkedin.com/in/benyamin-ben-hirschberg-66141890) | [@slashben](https://github.com/slashben) | [ARMO](https://www.armosec.io/) | 2021-09-01 |
|
||||
| [Rotem Refael](https://www.linkedin.com/in/rotem-refael) | [@rotemamsa](https://github.com/rotemamsa) | [ARMO](https://www.armosec.io/) | 2021-10-11 |
|
||||
| [David Wertenteil](https://www.linkedin.com/in/david-wertenteil-0ba277b9) | [@dwertent](https://github.com/dwertent) | [ARMO](https://www.armosec.io/) | 2021-09-01 |
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
Go to the [centralized MAINTAINERS.md](https://github.com/kubescape/project-governance/blob/main/MAINTAINERS.md)
|
||||
|
||||
480
README.md
480
README.md
@@ -3,11 +3,12 @@
|
||||
[](https://goreportcard.com/report/github.com/kubescape/kubescape)
|
||||
[](https://gitpod.io/#https://github.com/kubescape/kubescape)
|
||||
[](https://github.com/kubescape/kubescape/blob/master/LICENSE)
|
||||
[](https://landscape.cncf.io/card-mode?project=sandbox&selected=kubescape)
|
||||
[](https://landscape.cncf.io/?item=provisioning--security-compliance--kubescape)
|
||||
[](https://artifacthub.io/packages/search?repo=kubescape)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fkubescape%2Fkubescape?ref=badge_shield&issueType=license)
|
||||
[](https://www.bestpractices.dev/projects/6944)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/kubescape/kubescape)
|
||||
[](https://kubescape.io/docs/)
|
||||
[](https://github.com/kubescape/kubescape/stargazers)
|
||||
[](https://twitter.com/kubescape)
|
||||
[](https://cloud-native.slack.com/archives/C04EY3ZF9GE)
|
||||
@@ -20,99 +21,480 @@
|
||||
<img alt="Kubescape logo" align="right" src="https://raw.githubusercontent.com/cncf/artwork/master/projects/kubescape/stacked/color/kubescape-stacked-color.svg" width="150">
|
||||
</picture>
|
||||
|
||||
_An open-source Kubernetes security platform for your clusters, CI/CD pipelines, and IDE that seperates out the security signal from the scanner noise_
|
||||
_Comprehensive Kubernetes Security from Development to Runtime_
|
||||
|
||||
Kubescape is an open-source Kubernetes security platform, built for use in your day-to-day workflow, by fitting into your clusters, CI/CD pipelines and IDE. It serves as a one-stop-shop for Kuberenetes security and includes vulnerability and misconfiguration scanning. You can run scans via the CLI, or add the Kubescape Helm chart, which gives an in-depth view of what is going on in the cluster.
|
||||
Kubescape is an open-source Kubernetes security platform that provides comprehensive security coverage, from left to right across the entire development and deployment lifecycle. It offers hardening, posture management, and runtime security capabilities to ensure robust protection for Kubernetes environments.
|
||||
|
||||
Kubescape includes misconfiguration and vulnerability scanning as well as risk analysis and security compliance indicators. All results are presented in context and users get many cues on what to do based on scan results.Targeted at the DevSecOps practitioner or platform engineer, it offers an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities. It saves Kubernetes users and admins precious time, effort, and resources.
|
||||
Kubescape was created by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository) and is a [Cloud Native Computing Foundation (CNCF) incubating project](https://www.cncf.io/projects/).
|
||||
|
||||
Kubescape scans clusters, YAML files, and Helm charts. It detects misconfigurations according to multiple frameworks (including [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo/?utm_source=github&utm_medium=repository), [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) and the [CIS Benchmark](https://www.armosec.io/blog/cis-kubernetes-benchmark-framework-scanning-tools-comparison/?utm_source=github&utm_medium=repository)).
|
||||
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape!_
|
||||
|
||||
Kubescape was created by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository) and is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/).
|
||||
---
|
||||
|
||||
## Demo
|
||||
<img src="docs/img/demo-v3.gif">
|
||||
## 📑 Table of Contents
|
||||
|
||||
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape! 😀_
|
||||
- [Features](#-features)
|
||||
- [Demo](#-demo)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Installation](#-installation)
|
||||
- [CLI Commands](#-cli-commands)
|
||||
- [Usage Examples](#-usage-examples)
|
||||
- [Architecture](#-architecture)
|
||||
- [In-Cluster Operator](#-in-cluster-operator)
|
||||
- [Integrations](#-integrations)
|
||||
- [Community](#-community)
|
||||
- [Changelog](#changelog)
|
||||
- [License](#license)
|
||||
|
||||
## Getting started
|
||||
---
|
||||
|
||||
Experimenting with Kubescape is as easy as:
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔍 **Misconfiguration Scanning** | Scan clusters, YAML files, and Helm charts against NSA-CISA, MITRE ATT&CK®, and CIS Benchmarks |
|
||||
| 🐳 **Image Vulnerability Scanning** | Detect CVEs in container images using [Grype](https://github.com/anchore/grype) |
|
||||
| 🩹 **Image Patching** | Automatically patch vulnerable images using [Copacetic](https://github.com/project-copacetic/copacetic) |
|
||||
| 🔧 **Auto-Remediation** | Automatically fix misconfigurations in Kubernetes manifests |
|
||||
| 🛡️ **Admission Control** | Enforce security policies with Validating Admission Policies (VAP) |
|
||||
| 📊 **Runtime Security** | eBPF-based runtime monitoring via [Inspektor Gadget](https://github.com/inspektor-gadget) |
|
||||
| 🤖 **AI Integration** | MCP server for AI assistant integration |
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Demo
|
||||
|
||||
<img src="docs/img/demo-v3.gif" alt="Kubescape CLI demo">
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Install Kubescape
|
||||
|
||||
```sh
|
||||
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
||||
```
|
||||
|
||||
Learn more about:
|
||||
> 💡 See [Installation](#-installation) for more options (Homebrew, Krew, Windows, etc.)
|
||||
|
||||
* [Installing Kubescape](docs/installation.md)
|
||||
* [Running your first scan](docs/getting-started.md#run-your-first-scan)
|
||||
* [Usage](docs/getting-started.md#examples)
|
||||
* [Architecture](docs/architecture.md)
|
||||
* [Building Kubescape from source](https://github.com/kubescape/kubescape/wiki/Building)
|
||||
### 2. Run Your First Scan
|
||||
|
||||
_Did you know you can use Kubescape in all these places?_
|
||||
```sh
|
||||
# Scan your current cluster
|
||||
kubescape scan
|
||||
|
||||
# Scan a specific YAML file or directory
|
||||
kubescape scan /path/to/manifests/
|
||||
|
||||
# Scan a container image for vulnerabilities
|
||||
kubescape scan image nginx:latest
|
||||
```
|
||||
|
||||
### 3. Explore the Results
|
||||
|
||||
Kubescape provides a detailed security posture overview including:
|
||||
- Control plane security status
|
||||
- Access control risks
|
||||
- Workload misconfigurations
|
||||
- Network policy gaps
|
||||
- Compliance scores (MITRE, NSA)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### One-Line Install (Linux/macOS)
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
||||
```
|
||||
|
||||
### Package Managers
|
||||
|
||||
| Platform | Command |
|
||||
|----------|---------|
|
||||
| **Homebrew** | `brew install kubescape` |
|
||||
| **Krew** | `kubectl krew install kubescape` |
|
||||
| **Arch Linux** | `yay -S kubescape` |
|
||||
| **Ubuntu** | `sudo add-apt-repository ppa:kubescape/kubescape && sudo apt install kubescape` |
|
||||
| **NixOS** | `nix-shell -p kubescape` |
|
||||
| **Chocolatey** | `choco install kubescape` |
|
||||
| **Scoop** | `scoop install kubescape` |
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
iwr -useb https://raw.githubusercontent.com/kubescape/kubescape/master/install.ps1 | iex
|
||||
```
|
||||
|
||||
📖 **[Full Installation Guide →](docs/installation.md)**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ CLI Commands
|
||||
|
||||
Kubescape provides a comprehensive CLI with the following commands:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`kubescape scan`](#scanning) | Scan cluster, files, or images for security issues |
|
||||
| [`kubescape scan image`](#image-scanning) | Scan container images for vulnerabilities |
|
||||
| [`kubescape fix`](#auto-fix) | Auto-fix misconfigurations in manifest files |
|
||||
| [`kubescape patch`](#image-patching) | Patch container images to fix vulnerabilities |
|
||||
| [`kubescape list`](#list-frameworks-and-controls) | List available frameworks and controls |
|
||||
| [`kubescape download`](#offline-support) | Download artifacts for offline/air-gapped use |
|
||||
| [`kubescape config`](#configuration) | Manage cached configurations |
|
||||
| [`kubescape operator`](#operator-commands) | Interact with in-cluster Kubescape operator |
|
||||
| [`kubescape vap`](#validating-admission-policies) | Manage Validating Admission Policies |
|
||||
| [`kubescape mcpserver`](#mcp-server) | Start MCP server for AI assistant integration |
|
||||
| `kubescape completion` | Generate shell completion scripts |
|
||||
| `kubescape version` | Display version information |
|
||||
|
||||
---
|
||||
|
||||
## 📖 Usage Examples
|
||||
|
||||
### Scanning
|
||||
|
||||
#### Scan a Running Cluster
|
||||
|
||||
```bash
|
||||
# Default scan (all frameworks)
|
||||
kubescape scan
|
||||
|
||||
# Scan with a specific framework
|
||||
kubescape scan framework nsa
|
||||
kubescape scan framework mitre
|
||||
kubescape scan framework cis-v1.23-t1.0.1
|
||||
|
||||
# Scan a specific control
|
||||
kubescape scan control C-0005 -v
|
||||
```
|
||||
|
||||
#### Scan Files and Repositories
|
||||
|
||||
```bash
|
||||
# Scan local YAML files
|
||||
kubescape scan /path/to/manifests/
|
||||
|
||||
# Scan a Helm chart
|
||||
kubescape scan /path/to/helm/chart/
|
||||
|
||||
# Scan a Git repository
|
||||
kubescape scan https://github.com/kubescape/kubescape
|
||||
|
||||
# Scan with Kustomize
|
||||
kubescape scan /path/to/kustomize/directory/
|
||||
```
|
||||
|
||||
#### Scan Options
|
||||
|
||||
```bash
|
||||
# Include/exclude namespaces
|
||||
kubescape scan --include-namespaces production,staging
|
||||
kubescape scan --exclude-namespaces kube-system,kube-public
|
||||
|
||||
# Use alternative kubeconfig
|
||||
kubescape scan --kubeconfig /path/to/kubeconfig
|
||||
|
||||
# Set compliance threshold (exit code 1 if below threshold)
|
||||
kubescape scan --compliance-threshold 80
|
||||
|
||||
# Set severity threshold
|
||||
kubescape scan --severity-threshold high
|
||||
```
|
||||
|
||||
#### Output Formats
|
||||
|
||||
```bash
|
||||
# JSON output
|
||||
kubescape scan --format json --output results.json
|
||||
|
||||
# JUnit XML (for CI/CD)
|
||||
kubescape scan --format junit --output results.xml
|
||||
|
||||
# SARIF (for GitHub Code Scanning)
|
||||
kubescape scan --format sarif --output results.sarif
|
||||
|
||||
# HTML report
|
||||
kubescape scan --format html --output report.html
|
||||
|
||||
# PDF report
|
||||
kubescape scan --format pdf --output report.pdf
|
||||
```
|
||||
|
||||
### Image Scanning
|
||||
|
||||
```bash
|
||||
# Scan a public image
|
||||
kubescape scan image nginx:1.21
|
||||
|
||||
# Scan with verbose output
|
||||
kubescape scan image nginx:1.21 -v
|
||||
|
||||
# Scan a private registry image
|
||||
kubescape scan image myregistry/myimage:tag --username user --password pass
|
||||
```
|
||||
|
||||
### Auto-Fix
|
||||
|
||||
Automatically fix misconfigurations in your manifest files:
|
||||
|
||||
```bash
|
||||
# First, scan and save results to JSON
|
||||
kubescape scan /path/to/manifests --format json --output results.json
|
||||
|
||||
# Then apply fixes
|
||||
kubescape fix results.json
|
||||
|
||||
# Dry run (preview changes without applying)
|
||||
kubescape fix results.json --dry-run
|
||||
|
||||
# Apply fixes without confirmation prompts
|
||||
kubescape fix results.json --no-confirm
|
||||
```
|
||||
|
||||
### Image Patching
|
||||
|
||||
Patch container images to fix OS-level vulnerabilities:
|
||||
|
||||
```bash
|
||||
# Start buildkitd (required)
|
||||
sudo buildkitd &
|
||||
|
||||
# Patch an image
|
||||
sudo kubescape patch --image docker.io/library/nginx:1.22
|
||||
|
||||
# Specify custom output tag
|
||||
sudo kubescape patch --image nginx:1.22 --tag nginx:1.22-patched
|
||||
|
||||
# See detailed vulnerability report
|
||||
sudo kubescape patch --image nginx:1.22 -v
|
||||
```
|
||||
|
||||
📖 **[Full Patch Command Documentation →](cmd/patch/README.md)**
|
||||
|
||||
### List Frameworks and Controls
|
||||
|
||||
```bash
|
||||
# List available frameworks
|
||||
kubescape list frameworks
|
||||
|
||||
# List all controls
|
||||
kubescape list controls
|
||||
|
||||
# Output as JSON
|
||||
kubescape list controls --format json
|
||||
```
|
||||
|
||||
### Offline Support
|
||||
|
||||
Download artifacts for air-gapped environments:
|
||||
|
||||
```bash
|
||||
# Download all artifacts
|
||||
kubescape download artifacts --output /path/to/offline/dir
|
||||
|
||||
# Download a specific framework
|
||||
kubescape download framework nsa --output /path/to/nsa.json
|
||||
|
||||
# Scan using downloaded artifacts
|
||||
kubescape scan --use-artifacts-from /path/to/offline/dir
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# View current configuration
|
||||
kubescape config view
|
||||
|
||||
# Set account ID
|
||||
kubescape config set accountID <your-account-id>
|
||||
|
||||
# Delete cached configuration
|
||||
kubescape config delete
|
||||
```
|
||||
|
||||
### Operator Commands
|
||||
|
||||
Interact with the in-cluster Kubescape operator:
|
||||
|
||||
```bash
|
||||
# Trigger a configuration scan
|
||||
kubescape operator scan configurations
|
||||
|
||||
# Trigger a vulnerability scan
|
||||
kubescape operator scan vulnerabilities
|
||||
```
|
||||
|
||||
### Validating Admission Policies
|
||||
|
||||
Manage Kubernetes Validating Admission Policies:
|
||||
|
||||
```bash
|
||||
# Deploy the Kubescape CEL admission policy library
|
||||
kubescape vap deploy-library | kubectl apply -f -
|
||||
|
||||
# Create a policy binding
|
||||
kubescape vap create-policy-binding \
|
||||
--name my-policy-binding \
|
||||
--policy c-0016 \
|
||||
--namespace my-namespace | kubectl apply -f -
|
||||
```
|
||||
|
||||
### MCP Server
|
||||
|
||||
Start an MCP (Model Context Protocol) server for AI assistant integration:
|
||||
|
||||
```bash
|
||||
kubescape mcpserver
|
||||
```
|
||||
|
||||
The MCP server exposes Kubescape's vulnerability and configuration scan data to AI assistants, enabling natural language queries about your cluster's security posture.
|
||||
|
||||
**Available MCP Tools:**
|
||||
- `list_vulnerability_manifests` - Discover vulnerability manifests
|
||||
- `list_vulnerabilities_in_manifest` - List CVEs in a manifest
|
||||
- `list_vulnerability_matches_for_cve` - Get details for a specific CVE
|
||||
- `list_configuration_security_scan_manifests` - List configuration scan results
|
||||
- `get_configuration_security_scan_manifest` - Get configuration scan details
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
Kubescape can run in two modes:
|
||||
|
||||
### CLI Mode
|
||||
|
||||
The CLI is a standalone tool that scans clusters, files, and images on-demand.
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/img/ksfromcodetodeploy.png" alt="Places you can use Kubescape: in your IDE, CI, CD, or against a running cluster.">
|
||||
<img src="docs/img/ks-cli-arch.png" width="600" alt="CLI Architecture">
|
||||
</div>
|
||||
|
||||
## Kubescape-operator Helm-Chart
|
||||
**Key Components:**
|
||||
- **[Open Policy Agent (OPA)](https://github.com/open-policy-agent/opa)** - Policy evaluation engine
|
||||
- **[Regolibrary](https://github.com/kubescape/regolibrary)** - Library of security controls
|
||||
- **[Grype](https://github.com/anchore/grype)** - Image vulnerability scanning
|
||||
- **[Copacetic](https://github.com/project-copacetic/copacetic)** - Image patching
|
||||
|
||||
Besides the CLI, the Kubescape operator can also be installed via a Helm chart. Installing the Helm chart is an excellent way to begin using Kubescape, as it provides extensive features such as continuous scanning, image vulnerability scanning, runtime analysis, network policy generation, and more. You can find the Helm chart in the [Kubescape-operator documentation](https://kubescape.io/docs/install-operator/).
|
||||
### Operator Mode (In-Cluster)
|
||||
|
||||
## Kubescape GitHub Action
|
||||
For continuous monitoring, deploy the Kubescape operator via Helm.
|
||||
|
||||
Kubescape can be used as a GitHub Action. This is a great way to integrate Kubescape into your CI/CD pipeline. You can find the Kubescape GitHub Action in the [GitHub Action marketplace](https://github.com/marketplace/actions/kubescape).
|
||||
<div align="center">
|
||||
<img src="docs/img/ks-operator-arch.png" width="600" alt="Operator Architecture">
|
||||
</div>
|
||||
|
||||
## Under the hood
|
||||
**Additional Capabilities:**
|
||||
- Continuous configuration scanning
|
||||
- Image vulnerability scanning
|
||||
- Runtime analysis with eBPF
|
||||
- Network policy generation
|
||||
|
||||
Kubescape uses [Open Policy Agent](https://github.com/open-policy-agent/opa) to verify Kubernetes objects against [a library of posture controls](https://github.com/kubescape/regolibrary).
|
||||
📖 **[Full Architecture Documentation →](docs/architecture.md)**
|
||||
|
||||
By default, the results are printed in a console-friendly manner, but they can be:
|
||||
---
|
||||
|
||||
* exported to JSON or junit XML
|
||||
* rendered to HTML or PDF
|
||||
* submitted to a [cloud service](docs/providers.md)
|
||||
## ☸️ In-Cluster Operator
|
||||
|
||||
It retrieves Kubernetes objects from the API server and runs a set of [Rego snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io?utm_source=github&utm_medium=repository).
|
||||
The Kubescape operator provides continuous security monitoring in your cluster:
|
||||
|
||||
## Community
|
||||
```bash
|
||||
# Add the Kubescape Helm repository
|
||||
helm repo add kubescape https://kubescape.github.io/helm-charts/
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We are part of the Kubernetes community and are building more tests and controls as the ecosystem develops.
|
||||
# Install the operator
|
||||
helm upgrade --install kubescape kubescape/kubescape-operator \
|
||||
--namespace kubescape \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
We hold [community meetings](https://zoom.us/j/95174063585) on Zoom, on the first Tuesday of every month, at 14:00 GMT. ([See that in your local time zone](https://time.is/compare/1400_in_GMT)).
|
||||
**Operator Features:**
|
||||
- 🔄 Continuous misconfiguration scanning
|
||||
- 🐳 Image vulnerability scanning for all workloads
|
||||
- 🔍 Runtime threat detection (eBPF-based)
|
||||
- 🌐 Network policy generation
|
||||
- 📈 Prometheus metrics integration
|
||||
|
||||
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
📖 **[Operator Installation Guide →](https://kubescape.io/docs/operator/)**
|
||||
|
||||
### Adopters
|
||||
---
|
||||
|
||||
See [here](ADOPTERS.md) a list of adopters.
|
||||
## 🔌 Integrations
|
||||
|
||||
## Contributions
|
||||
### CI/CD
|
||||
|
||||
Thanks to all our contributors! Check out our [CONTRIBUTING](CONTRIBUTING.md) file to learn how to join them.
|
||||
| Platform | Integration |
|
||||
|----------|-------------|
|
||||
| **GitHub Actions** | [kubescape/github-action](https://github.com/marketplace/actions/kubescape) |
|
||||
| **GitLab CI** | [Documentation](https://kubescape.io/docs/integrations/gitlab/) |
|
||||
| **Jenkins** | [Documentation](https://kubescape.io/docs/integrations/jenkins/) |
|
||||
|
||||
* Feel free to pick a task from the [issues](https://github.com/kubescape/kubescape/issues?q=is%3Aissue+is%3Aopen+label%3A%22open+for+contribution%22), [roadmap](docs/roadmap.md) or suggest a feature of your own.
|
||||
* [Open an issue](https://github.com/kubescape/kubescape/issues/new/choose): we aim to respond to all issues within 48 hours.
|
||||
* [Join the CNCF Slack](https://slack.cncf.io/) and then our [users](https://cloud-native.slack.com/archives/C04EY3ZF9GE) or [developers](https://cloud-native.slack.com/archives/C04GY6H082K) channel.
|
||||
### IDE Extensions
|
||||
|
||||
<br>
|
||||
| IDE | Extension |
|
||||
|-----|-----------|
|
||||
| **VS Code** | [Kubescape Extension](https://marketplace.visualstudio.com/items?itemName=kubescape.kubescape) |
|
||||
| **Lens** | [Kubescape Lens Extension](https://github.com/armosec/lens-kubescape) |
|
||||
|
||||
<a href = "https://github.com/kubescape/kubescape/graphs/contributors">
|
||||
<img src = "https://contrib.rocks/image?repo=kubescape/kubescape"/>
|
||||
### Where You Can Use Kubescape
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/img/ksfromcodetodeploy.png" alt="Kubescape integration points: IDE, CI, CD, Runtime">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 👥 Community
|
||||
|
||||
Kubescape is a CNCF incubating project with an active community.
|
||||
|
||||
### Get Involved
|
||||
|
||||
- 💬 **[Slack - Users Channel](https://cloud-native.slack.com/archives/C04EY3ZF9GE)** - Ask questions, get help
|
||||
- 💬 **[Slack - Developers Channel](https://cloud-native.slack.com/archives/C04GY6H082K)** - Contribute to development
|
||||
- 🐛 **[GitHub Issues](https://github.com/kubescape/kubescape/issues)** - Report bugs and request features
|
||||
- 📋 **[Project Board](https://github.com/orgs/kubescape/projects/4)** - See what we're working on
|
||||
- 🗺️ **[Roadmap](https://github.com/kubescape/project-governance/blob/main/ROADMAP.md)** - Future plans
|
||||
|
||||
### Contributing
|
||||
|
||||
We welcome contributions! Please see our:
|
||||
- **[Contributing Guide](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md)**
|
||||
- **[Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md)**
|
||||
|
||||
### Community Resources
|
||||
|
||||
- **[Community Info](https://github.com/kubescape/project-governance/blob/main/COMMUNITY.md)**
|
||||
- **[Governance](https://github.com/kubescape/project-governance/blob/main/GOVERNANCE.md)**
|
||||
- **[Security Policy](https://github.com/kubescape/project-governance/blob/main/SECURITY.md)**
|
||||
- **[Maintainers](https://github.com/kubescape/project-governance/blob/main/MAINTAINERS.md)**
|
||||
|
||||
### Contributors
|
||||
|
||||
<a href="https://github.com/kubescape/kubescape/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=kubescape/kubescape"/>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
Kubescape changes are tracked on the [release](https://github.com/kubescape/kubescape/releases) page
|
||||
Kubescape changes are tracked on the [releases page](https://github.com/kubescape/kubescape/releases).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2021-2023, the Kubescape Authors. All rights reserved. Kubescape is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||
Copyright 2021-2025, the Kubescape Authors. All rights reserved.
|
||||
|
||||
Kubescape is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/) and was contributed by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository).
|
||||
Kubescape is released under the [Apache 2.0 license](LICENSE).
|
||||
|
||||
Kubescape is a [Cloud Native Computing Foundation (CNCF) incubating project](https://www.cncf.io/projects/kubescape/) and was contributed by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository).
|
||||
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/cncf/artwork/master/other/cncf-sandbox/horizontal/color/cncf-sandbox-horizontal-color.svg" width="300" alt="CNCF Sandbox Project">
|
||||
</div>
|
||||
<img src="https://raw.githubusercontent.com/cncf/artwork/refs/heads/main/other/cncf-member/incubating/color/cncf-incubating-color.svg" width="300" alt="CNCF Incubating Project">
|
||||
</div>
|
||||
@@ -4,15 +4,19 @@ header:
|
||||
last-reviewed: '2023-10-12'
|
||||
expiration-date: '2024-10-12T01:00:00.000Z'
|
||||
project-url: https://github.com/kubescape/kubescape/
|
||||
project-release: '1.0.0'
|
||||
project-release: 1.0.0
|
||||
project-lifecycle:
|
||||
status: active
|
||||
bug-fixes-only: false
|
||||
core-maintainers:
|
||||
- github:slashben
|
||||
- github:amirmalka
|
||||
- github:amitschendel
|
||||
- github:bezbran
|
||||
- github:craigbox
|
||||
- github:matthyx
|
||||
- github:dwertent
|
||||
- github:matthyx
|
||||
- github:rotemamsa
|
||||
- github:slashben
|
||||
contribution-policy:
|
||||
accepts-pull-requests: true
|
||||
accepts-automated-pull-requests: false
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Reporting Security Issues
|
||||
# Security
|
||||
|
||||
To report a security issue or vulnerability, submit a [private vulnerability report via GitHub](https://github.com/kubescape/kubescape/security/advisories/new) to the repository maintainers with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
|
||||
The Kubescape project manages this document in the central project repository.
|
||||
|
||||
The maintainers will respond within 7 working days of your report. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline.
|
||||
|
||||
Other contacts: cncf-kubescape-maintainers@lists.cncf.io
|
||||
Go to the [centralized SECURITY.md](https://github.com/kubescape/project-governance/blob/main/SECURITY.md)
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21-bullseye as builder
|
||||
|
||||
ENV GO111MODULE=on CGO_ENABLED=0
|
||||
WORKDIR /work
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
RUN --mount=target=. \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
cd httphandler && GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/ksserver .
|
||||
|
||||
FROM gcr.io/distroless/static-debian11:nonroot
|
||||
FROM gcr.io/distroless/static-debian13:nonroot
|
||||
|
||||
USER nonroot
|
||||
WORKDIR /home/nonroot/
|
||||
|
||||
COPY --from=builder /out/ksserver /usr/bin/ksserver
|
||||
ARG TARGETPLATFORM
|
||||
COPY $TARGETPLATFORM/downloader /usr/bin/downloader
|
||||
RUN ["downloader"]
|
||||
COPY $TARGETPLATFORM/ksserver /usr/bin/ksserver
|
||||
|
||||
ARG image_version client
|
||||
ENV RELEASE=$image_version CLIENT=$client
|
||||
|
||||
244
build/README.md
244
build/README.md
@@ -1,19 +1,241 @@
|
||||
## Docker Build
|
||||
# Building Kubescape
|
||||
|
||||
### Build your own Docker image
|
||||
This guide covers how to build Kubescape from source.
|
||||
|
||||
1. Clone Project
|
||||
```
|
||||
git clone https://github.com/kubescape/kubescape.git kubescape && cd "$_"
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Building the CLI](#building-the-cli)
|
||||
- [Building Docker Images](#building-docker-images)
|
||||
- [Build Options](#build-options)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
|
||||
- **Go 1.23+** - [Installation Guide](https://golang.org/doc/install)
|
||||
- **Git** - For cloning the repository
|
||||
- **Make** - For running build commands
|
||||
|
||||
### Optional (for Docker builds)
|
||||
|
||||
- **Docker** - [Installation Guide](https://docs.docker.com/get-docker/)
|
||||
- **Docker Buildx** - For multi-platform builds (included with Docker Desktop)
|
||||
- **GoReleaser** - [Installation Guide](https://goreleaser.com/install/)
|
||||
|
||||
### Verify Prerequisites
|
||||
|
||||
```bash
|
||||
go version # Should be 1.23 or higher
|
||||
git --version
|
||||
make --version
|
||||
docker --version # Optional
|
||||
goreleaser --version # Optional
|
||||
```
|
||||
|
||||
2. Build kubescape CLI Docker image
|
||||
```
|
||||
make all
|
||||
docker buildx build -t kubescape-cli -f build/kubescape-cli.Dockerfile --build-arg="ks_binary=kubescape" --load .
|
||||
---
|
||||
|
||||
## Building the CLI
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kubescape/kubescape.git
|
||||
cd kubescape
|
||||
```
|
||||
|
||||
3. Build kubescape Docker image
|
||||
### Build with Make
|
||||
|
||||
```bash
|
||||
# Build for your current platform
|
||||
make build
|
||||
|
||||
# The binary will be at ./kubescape
|
||||
./kubescape version
|
||||
```
|
||||
docker buildx build -t kubescape -f build/Dockerfile --load .
|
||||
|
||||
### Build Directly with Go
|
||||
|
||||
```bash
|
||||
go build -o kubescape .
|
||||
```
|
||||
|
||||
### Build with GoReleaser
|
||||
|
||||
```bash
|
||||
# Build for your current platform
|
||||
RELEASE=v0.0.1 CLIENT=local goreleaser build --snapshot --clean --single-target
|
||||
```
|
||||
|
||||
### Cross-Compilation
|
||||
|
||||
Build for different platforms:
|
||||
|
||||
```bash
|
||||
# Linux (amd64)
|
||||
GOOS=linux GOARCH=amd64 go build -o kubescape-linux-amd64 .
|
||||
|
||||
# Linux (arm64)
|
||||
GOOS=linux GOARCH=arm64 go build -o kubescape-linux-arm64 .
|
||||
|
||||
# macOS (amd64)
|
||||
GOOS=darwin GOARCH=amd64 go build -o kubescape-darwin-amd64 .
|
||||
|
||||
# macOS (arm64 / Apple Silicon)
|
||||
GOOS=darwin GOARCH=arm64 go build -o kubescape-darwin-arm64 .
|
||||
|
||||
# Windows (amd64)
|
||||
GOOS=windows GOARCH=amd64 go build -o kubescape-windows-amd64.exe .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building Docker Images
|
||||
|
||||
Kubescape uses [GoReleaser](https://goreleaser.com/) to build its Docker images. The Dockerfiles are specifically designed to work with GoReleaser's build pipeline, which handles cross-compilation and places binaries in the expected directory structure.
|
||||
|
||||
### Build with GoReleaser
|
||||
|
||||
The recommended way to build Docker images locally is using GoReleaser. Note that `RELEASE`, `CLIENT`, and `RUN_E2E` environment variables are required:
|
||||
|
||||
```bash
|
||||
# Build all artifacts and Docker images locally without publishing
|
||||
# --skip=before,krew,nfpm,sbom skips unnecessary steps for faster local builds
|
||||
RELEASE=v0.0.1 CLIENT=local RUN_E2E=false goreleaser release --snapshot --clean --skip=before,nfpm,sbom
|
||||
```
|
||||
|
||||
Please read the [GoReleaser documentation](https://goreleaser.com/customization/dockers_v2/#testing-locally) for more details on using it for local testing.
|
||||
|
||||
---
|
||||
|
||||
## Build Options
|
||||
|
||||
### Make Targets
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `make build` | Build the Kubescape binary |
|
||||
| `make test` | Run unit tests |
|
||||
| `make all` | Build everything |
|
||||
| `make clean` | Remove build artifacts |
|
||||
|
||||
### Build Tags
|
||||
|
||||
You can use Go build tags to customize the build:
|
||||
|
||||
```bash
|
||||
# Example with build tags
|
||||
go build -tags "netgo" -o kubescape .
|
||||
```
|
||||
|
||||
### Version Information
|
||||
|
||||
To embed version information in the build:
|
||||
|
||||
```bash
|
||||
VERSION=$(git describe --tags --always)
|
||||
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
COMMIT=$(git rev-parse HEAD)
|
||||
|
||||
go build -ldflags "-X main.version=$VERSION -X main.buildDate=$BUILD_DATE -X main.commit=$COMMIT" -o kubescape .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Install Development Dependencies
|
||||
|
||||
```bash
|
||||
# Install golangci-lint for linting
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
# Install other tools as needed
|
||||
go mod download
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run tests with coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Run specific package tests
|
||||
go test ./core/...
|
||||
```
|
||||
|
||||
### Run Linter
|
||||
|
||||
```bash
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
### Code Formatting
|
||||
|
||||
```bash
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails with "module not found"
|
||||
|
||||
```bash
|
||||
# Update dependencies
|
||||
go mod tidy
|
||||
go mod download
|
||||
```
|
||||
|
||||
### CGO-related Errors
|
||||
|
||||
If you encounter CGO errors, try building with CGO disabled:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -o kubescape .
|
||||
```
|
||||
|
||||
### Docker Build Fails
|
||||
|
||||
Ensure Docker daemon is running and you have sufficient permissions.
|
||||
|
||||
If you encounter an error like `failed to calculate checksum ... "/linux/amd64/kubescape": not found`, it usually means you are trying to run `docker build` manually. Because the Dockerfiles are optimized for GoReleaser, you should use the `goreleaser release --snapshot` command described in the [Building Docker Images](#building-docker-images) section instead.
|
||||
|
||||
```bash
|
||||
# Check Docker status
|
||||
docker info
|
||||
```
|
||||
|
||||
### Out of Memory During Build
|
||||
|
||||
For systems with limited memory:
|
||||
|
||||
```bash
|
||||
# Limit Go's memory usage
|
||||
GOGC=50 go build -o kubescape .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dockerfiles
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `build/Dockerfile` | Full Kubescape image with HTTP handler |
|
||||
| `build/kubescape-cli.Dockerfile` | Minimal CLI-only image |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Contributing Guide](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md)
|
||||
- [Architecture](../docs/architecture.md)
|
||||
- [Getting Started](../docs/getting-started.md)
|
||||
|
||||
151
build/goreleaser-post-e2e.sh
Normal file
151
build/goreleaser-post-e2e.sh
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env sh
|
||||
#
|
||||
# goreleaser-post-e2e.sh
|
||||
#
|
||||
# A small, robust POSIX shell script intended to be called from the goreleaser
|
||||
# `builds[].hooks.post` entry. It is responsible for optionally running the
|
||||
# repository smoke tests against the artifact produced in `dist/`.
|
||||
#
|
||||
# Usage:
|
||||
# RUN_E2E=true -> enable running smoke tests
|
||||
# E2E_FAIL_ON_ERROR=1 -> (default) treat test failures as fatal (exit non-zero)
|
||||
# E2E_FAIL_ON_ERROR=0 -> treat test failures as non-fatal (log, but exit 0)
|
||||
#
|
||||
# The script is written to be defensive and to work under /bin/sh on CI runners.
|
||||
# Use POSIX-safe flags only.
|
||||
set -eu
|
||||
|
||||
# Helper for logging
|
||||
_now() {
|
||||
date --iso-8601=seconds 2>/dev/null || date
|
||||
}
|
||||
log() {
|
||||
printf '%s [goreleaser-post-e2e] %s\n' "$(_now)" "$*"
|
||||
}
|
||||
|
||||
# GitHub Actions log grouping helpers (no-op outside Actions)
|
||||
gha_group_start() {
|
||||
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
|
||||
# Titles must be on a single line
|
||||
printf '::group::%s\n' "$*"
|
||||
fi
|
||||
}
|
||||
gha_group_end() {
|
||||
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
|
||||
printf '::endgroup::\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# Small helper to interpret various truthy forms (1/true/yes/y)
|
||||
is_true() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|yes|YES|y|Y) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Determine repo root relative to this script (script is expected to live in kubescape/build/)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
: "${RUN_E2E:=false}"
|
||||
# Default to fatal E2E failures.
|
||||
: "${E2E_FAIL_ON_ERROR:=1}"
|
||||
|
||||
log "Starting goreleaser post-build e2e script"
|
||||
log "RUN_E2E=${RUN_E2E}"
|
||||
log "E2E_FAIL_ON_ERROR=${E2E_FAIL_ON_ERROR}"
|
||||
|
||||
# Only run on linux/amd64 to avoid running multiple times (once per build)
|
||||
# and to ensure we can run the binary on the current host (assuming host is amd64).
|
||||
if [ -n "${GOARCH:-}" ] && [ "${GOARCH}" != "amd64" ]; then
|
||||
log "Skipping smoke tests for non-amd64 build (GOARCH=${GOARCH})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! is_true "${RUN_E2E}"; then
|
||||
log "RUN_E2E is not enabled. Skipping smoke tests. (RUN_E2E=${RUN_E2E})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate the amd64 artifact in dist/.
|
||||
# Goreleaser v2 puts binaries in dist/<id>_<os>_<arch>_<version>/<binary>
|
||||
# Example: dist/cli_linux_amd64_v1/kubescape
|
||||
ART_PATH=""
|
||||
if [ -d "$REPO_ROOT/dist" ]; then
|
||||
# Find any file named 'kubescape' inside a directory containing 'linux_amd64' inside 'dist'
|
||||
# We use 'find' for robustness against varying directory names
|
||||
ART_PATH=$(find "$REPO_ROOT/dist" -type f -name "kubescape" -path "*linux_amd64*" | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -z "$ART_PATH" ] || [ ! -f "$ART_PATH" ]; then
|
||||
log "No kubescape artifact found in dist/ matching *linux_amd64*/kubescape. Skipping smoke tests."
|
||||
# If we are supposed to run E2E, not finding the artifact is probably an error.
|
||||
if is_true "${E2E_FAIL_ON_ERROR}"; then
|
||||
log "E2E_FAIL_ON_ERROR enabled -> failing because artifact was not found."
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Using artifact: $ART_PATH"
|
||||
# Make binary executable if it is a binary
|
||||
chmod +x "$ART_PATH" >/dev/null 2>&1 || true
|
||||
|
||||
# Locate python runner
|
||||
PYTHON=""
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON=python3
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON=python
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
log "python3 (or python) not found in PATH."
|
||||
if is_true "${E2E_FAIL_ON_ERROR}"; then
|
||||
log "E2E_FAIL_ON_ERROR enabled -> failing the release because python is missing."
|
||||
exit 2
|
||||
else
|
||||
log "E2E_FAIL_ON_ERROR disabled -> continuing without running tests."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for smoke test runner
|
||||
SMOKE_RUNNER="$REPO_ROOT/smoke_testing/init.py"
|
||||
if [ ! -f "$SMOKE_RUNNER" ]; then
|
||||
log "Smoke test runner not found at $SMOKE_RUNNER"
|
||||
if is_true "${E2E_FAIL_ON_ERROR}"; then
|
||||
log "E2E_FAIL_ON_ERROR enabled -> failing the release because smoke runner is missing."
|
||||
exit 3
|
||||
else
|
||||
log "E2E_FAIL_ON_ERROR disabled -> continuing without running tests."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
gha_group_start "Smoke tests"
|
||||
log "Running smoke tests with $PYTHON $SMOKE_RUNNER \"$ART_PATH\""
|
||||
# Run the test runner, propagate exit code
|
||||
set +e
|
||||
"$PYTHON" "$SMOKE_RUNNER" "$ART_PATH"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
log "Smoke tests passed (exit code 0)."
|
||||
fi
|
||||
|
||||
log "Smoke tests exited with code: $rc"
|
||||
gha_group_end
|
||||
|
||||
if [ $rc -ne 0 ]; then
|
||||
if is_true "${E2E_FAIL_ON_ERROR}"; then
|
||||
log "E2E_FAIL_ON_ERROR enabled -> failing the release (exit code $rc)."
|
||||
exit $rc
|
||||
else
|
||||
log "E2E_FAIL_ON_ERROR disabled -> continuing despite test failures."
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM gcr.io/distroless/base-debian11:debug-nonroot
|
||||
FROM gcr.io/distroless/static-debian13:debug-nonroot
|
||||
|
||||
USER nonroot
|
||||
WORKDIR /home/nonroot/
|
||||
@@ -6,7 +6,8 @@ WORKDIR /home/nonroot/
|
||||
ARG image_version client TARGETARCH
|
||||
ENV RELEASE=$image_version CLIENT=$client
|
||||
|
||||
COPY kubescape-${TARGETARCH}-ubuntu-latest /usr/bin/kubescape
|
||||
ARG TARGETPLATFORM
|
||||
COPY $TARGETPLATFORM/kubescape /usr/bin/kubescape
|
||||
RUN ["kubescape", "download", "artifacts"]
|
||||
|
||||
ENTRYPOINT ["kubescape"]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -15,7 +13,7 @@ func getDeleteCmd(ks meta.IKubescape) *cobra.Command {
|
||||
Short: "Delete cached configurations",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := ks.DeleteCachedConfig(context.TODO(), &v1.DeleteConfig{}); err != nil {
|
||||
if err := ks.DeleteCachedConfig(&v1.DeleteConfig{}); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -3,7 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/core"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -74,11 +73,9 @@ func GetDownloadCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
downloadInfo.Target = args[0]
|
||||
if len(args) >= 2 {
|
||||
|
||||
downloadInfo.Identifier = args[1]
|
||||
|
||||
}
|
||||
if err := ks.Download(context.TODO(), &downloadInfo); err != nil {
|
||||
if err := ks.Download(&downloadInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -36,7 +34,7 @@ func GetFixCmd(ks meta.IKubescape) *cobra.Command {
|
||||
}
|
||||
fixInfo.ReportFile = args[0]
|
||||
|
||||
return ks.Fix(context.TODO(), &fixInfo)
|
||||
return ks.Fix(&fixInfo)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/core"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,7 +26,7 @@ var (
|
||||
%[1]s list controls
|
||||
|
||||
Control documentation:
|
||||
https://hub.armosec.io/docs/controls
|
||||
https://kubescape.io/docs/controls/
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
@@ -62,7 +61,7 @@ func GetListCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
listPolicies.Target = args[0]
|
||||
|
||||
if err := ks.List(context.TODO(), &listPolicies); err != nil {
|
||||
if err := ks.List(&listPolicies); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
466
cmd/mcpserver/mcpserver.go
Normal file
466
cmd/mcpserver/mcpserver.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/go-logger"
|
||||
helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers"
|
||||
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
|
||||
spdxv1beta1 "github.com/kubescape/storage/pkg/generated/clientset/versioned/typed/softwarecomposition/v1beta1"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type KubescapeMcpserver struct {
|
||||
s *server.MCPServer
|
||||
ksClient spdxv1beta1.SpdxV1beta1Interface
|
||||
}
|
||||
|
||||
func createVulnerabilityToolsAndResources(ksServer *KubescapeMcpserver) {
|
||||
// Tool to list vulnerability manifests
|
||||
listManifestsTool := mcp.NewTool(
|
||||
"list_vulnerability_manifests",
|
||||
mcp.WithDescription("Discover available vulnerability manifests at image and workload levels"),
|
||||
mcp.WithString("namespace",
|
||||
mcp.Description("Filter by namespace (optional)"),
|
||||
),
|
||||
mcp.WithString("level",
|
||||
mcp.Description("Type of vulnerability manifests to list"),
|
||||
mcp.Enum("image", "workload", "both"),
|
||||
),
|
||||
)
|
||||
|
||||
ksServer.s.AddTool(listManifestsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return ksServer.CallTool("list_vulnerability_manifests", request.Params.Arguments.(map[string]interface{}))
|
||||
})
|
||||
|
||||
listVulnerabilitiesTool := mcp.NewTool(
|
||||
"list_vulnerabilities_in_manifest",
|
||||
mcp.WithDescription("List all vulnerabilities in a given manifest"),
|
||||
mcp.WithString("namespace",
|
||||
mcp.Description("Filter by namespace (optional)"),
|
||||
),
|
||||
mcp.WithString("manifest_name",
|
||||
mcp.Required(),
|
||||
mcp.Description("Name of the manifest to list vulnerabilities from"),
|
||||
),
|
||||
)
|
||||
|
||||
ksServer.s.AddTool(listVulnerabilitiesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return ksServer.CallTool("list_vulnerabilities_in_manifest", request.Params.Arguments.(map[string]interface{}))
|
||||
})
|
||||
|
||||
listVulnerabilityMatchesForCVE := mcp.NewTool(
|
||||
"list_vulnerability_matches_for_cve",
|
||||
mcp.WithDescription("List all vulnerability matches for a given CVE in a given manifest"),
|
||||
mcp.WithString("namespace",
|
||||
mcp.Description("Filter by namespace (optional)"),
|
||||
),
|
||||
mcp.WithString("manifest_name",
|
||||
mcp.Required(),
|
||||
mcp.Description("Name of the manifest to list vulnerabilities from"),
|
||||
),
|
||||
mcp.WithString("cve_id",
|
||||
mcp.Required(),
|
||||
mcp.Description("ID of the CVE to list matches for"),
|
||||
),
|
||||
)
|
||||
|
||||
ksServer.s.AddTool(listVulnerabilityMatchesForCVE, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return ksServer.CallTool("list_vulnerability_matches_for_cve", request.Params.Arguments.(map[string]interface{}))
|
||||
})
|
||||
|
||||
vulnerabilityManifestTemplate := mcp.NewResourceTemplate(
|
||||
"kubescape://vulnerability-manifests/{namespace}/{manifest_name}",
|
||||
"Vulnerability Manifest",
|
||||
mcp.WithTemplateDescription("Complete vulnerability manifest either for a specific workload or image. Use 'list_vulnerability_manifests' tool to discover available manifests."),
|
||||
mcp.WithTemplateMIMEType("application/json"),
|
||||
)
|
||||
|
||||
ksServer.s.AddResourceTemplate(vulnerabilityManifestTemplate, ksServer.ReadResource)
|
||||
|
||||
}
|
||||
|
||||
func createConfigurationsToolsAndResources(ksServer *KubescapeMcpserver) {
|
||||
// Tool to list configuration manifests
|
||||
listConfigsTool := mcp.NewTool(
|
||||
"list_configuration_security_scan_manifests",
|
||||
mcp.WithDescription("Discover available security configuration scan results at workload level (this returns a list of manifests, not the scan results themselves, to get the scan results, use the get_configuration_security_scan_manifest tool)"),
|
||||
mcp.WithString("namespace",
|
||||
mcp.Description("Filter by namespace (optional)"),
|
||||
),
|
||||
)
|
||||
|
||||
ksServer.s.AddTool(listConfigsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return ksServer.CallTool("list_configuration_security_scan_manifests", request.Params.Arguments.(map[string]interface{}))
|
||||
})
|
||||
|
||||
getConfigDetailsTool := mcp.NewTool(
|
||||
"get_configuration_security_scan_manifest",
|
||||
mcp.WithDescription("Get details of a specific security configuration scan result"),
|
||||
mcp.WithString("namespace",
|
||||
mcp.Description("Namespace of the manifest (optional, defaults to 'kubescape')"),
|
||||
),
|
||||
mcp.WithString("manifest_name",
|
||||
mcp.Required(),
|
||||
mcp.Description("Name of the configuration manifest to get details for (get this from the list_configuration_security_scan_manifests tool)"),
|
||||
),
|
||||
)
|
||||
|
||||
ksServer.s.AddTool(getConfigDetailsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return ksServer.CallTool("get_configuration_security_scan_manifest", request.Params.Arguments.(map[string]interface{}))
|
||||
})
|
||||
|
||||
configManifestTemplate := mcp.NewResourceTemplate(
|
||||
"kubescape://configuration-manifests/{namespace}/{manifest_name}",
|
||||
"Configuration Security Scan Manifest",
|
||||
mcp.WithTemplateDescription("Complete configuration scan manifest for a specific workload. Use 'list_configuration_security_scan_manifests' tool to discover available manifests."),
|
||||
mcp.WithTemplateMIMEType("application/json"),
|
||||
)
|
||||
|
||||
ksServer.s.AddResourceTemplate(configManifestTemplate, ksServer.ReadConfigurationResource)
|
||||
}
|
||||
|
||||
func (ksServer *KubescapeMcpserver) ReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
||||
uri := request.Params.URI
|
||||
// Validate the URI and check if it starts with kubescape://vulnerability-manifests/
|
||||
if !strings.HasPrefix(uri, "kubescape://vulnerability-manifests/") {
|
||||
return nil, fmt.Errorf("invalid URI: %s", uri)
|
||||
}
|
||||
|
||||
// Verify that the URI is either the CVE list or CVE details
|
||||
if !strings.HasSuffix(uri, "/cve_list") && !strings.Contains(uri, "/cve_details/") {
|
||||
return nil, fmt.Errorf("invalid URI: %s", uri)
|
||||
}
|
||||
|
||||
// Split the URI into namespace and manifest name
|
||||
parts := strings.Split(uri, "/")
|
||||
if len(parts) != 4 && len(parts) != 5 {
|
||||
return nil, fmt.Errorf("invalid URI: %s", uri)
|
||||
}
|
||||
|
||||
namespace := parts[1]
|
||||
manifestName := parts[2]
|
||||
cveID := ""
|
||||
if len(parts) == 5 {
|
||||
cveID = parts[3]
|
||||
}
|
||||
|
||||
// Get the vulnerability manifest
|
||||
manifest, err := ksServer.ksClient.VulnerabilityManifests(namespace).Get(ctx, manifestName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vulnerability manifest: %s", err)
|
||||
}
|
||||
|
||||
var responseJson []byte
|
||||
if cveID == "" {
|
||||
// CVE list
|
||||
var cveList []v1beta1.Vulnerability
|
||||
for _, match := range manifest.Spec.Payload.Matches {
|
||||
cveList = append(cveList, match.Vulnerability)
|
||||
}
|
||||
responseJson, err = json.Marshal(cveList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cve list: %s", err)
|
||||
}
|
||||
} else {
|
||||
// CVE details
|
||||
var match []v1beta1.Match
|
||||
for _, m := range manifest.Spec.Payload.Matches {
|
||||
if m.Vulnerability.ID == cveID {
|
||||
match = append(match, m)
|
||||
}
|
||||
}
|
||||
responseJson, err = json.Marshal(match)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cve details: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return []mcp.ResourceContents{mcp.TextResourceContents{
|
||||
URI: uri,
|
||||
Text: string(responseJson),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (ksServer *KubescapeMcpserver) ReadConfigurationResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
||||
uri := request.Params.URI
|
||||
if !strings.HasPrefix(uri, "kubescape://configuration-manifests/") {
|
||||
return nil, fmt.Errorf("invalid URI: %s", uri)
|
||||
}
|
||||
parts := strings.Split(uri[len("kubescape://configuration-manifests/"):], "/")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid URI: %s", uri)
|
||||
}
|
||||
namespace := parts[0]
|
||||
manifestName := parts[1]
|
||||
manifest, err := ksServer.ksClient.WorkloadConfigurationScans(namespace).Get(ctx, manifestName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configuration manifest: %s", err)
|
||||
}
|
||||
responseJson, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configuration manifest: %s", err)
|
||||
}
|
||||
return []mcp.ResourceContents{mcp.TextResourceContents{
|
||||
URI: uri,
|
||||
Text: string(responseJson),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (ksServer *KubescapeMcpserver) CallTool(name string, arguments map[string]interface{}) (*mcp.CallToolResult, error) {
|
||||
switch name {
|
||||
case "list_vulnerability_manifests":
|
||||
//namespace, ok := arguments["namespace"]
|
||||
//if !ok {
|
||||
// namespace = ""
|
||||
//}
|
||||
level, ok := arguments["level"]
|
||||
if !ok {
|
||||
level = "both"
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"vulnerability_manifests": map[string]interface{}{},
|
||||
}
|
||||
|
||||
// Get workload-level manifests
|
||||
labelSelector := ""
|
||||
if level == "workload" {
|
||||
labelSelector = "kubescape.io/context=filtered"
|
||||
} else if level == "image" {
|
||||
labelSelector = "kubescape.io/context=non-filtered"
|
||||
}
|
||||
|
||||
var manifests *v1beta1.VulnerabilityManifestList
|
||||
var err error
|
||||
if labelSelector == "" {
|
||||
manifests, err = ksServer.ksClient.VulnerabilityManifests(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
|
||||
} else {
|
||||
manifests, err = ksServer.ksClient.VulnerabilityManifests(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{
|
||||
LabelSelector: labelSelector,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Found %d manifests", len(manifests.Items))
|
||||
|
||||
vulnerabilityManifests := []map[string]interface{}{}
|
||||
for _, manifest := range manifests.Items {
|
||||
isImageLevel := manifest.Annotations[helpersv1.WlidMetadataKey] == ""
|
||||
manifestMap := map[string]interface{}{
|
||||
"type": "workload",
|
||||
"namespace": manifest.Namespace,
|
||||
"manifest_name": manifest.Name,
|
||||
"image-level": isImageLevel,
|
||||
"workload-level": !isImageLevel,
|
||||
"image-id": manifest.Annotations[helpersv1.ImageIDMetadataKey],
|
||||
"image-tag": manifest.Annotations[helpersv1.ImageTagMetadataKey],
|
||||
"workload-id": manifest.Annotations[helpersv1.WlidMetadataKey],
|
||||
"workload-container-name": manifest.Annotations[helpersv1.ContainerNameMetadataKey],
|
||||
"resource_uri": fmt.Sprintf("kubescape://vulnerability-manifests/%s/%s",
|
||||
manifest.Namespace, manifest.Name),
|
||||
}
|
||||
vulnerabilityManifests = append(vulnerabilityManifests, manifestMap)
|
||||
}
|
||||
result["vulnerability_manifests"].(map[string]interface{})["manifests"] = vulnerabilityManifests
|
||||
|
||||
// Add template information
|
||||
result["available_templates"] = map[string]string{
|
||||
"vulnerability_manifest_cve_list": "kubescape://vulnerability-manifests/{namespace}/{manifest_name}/cve_list",
|
||||
"vulnerability_manifest_cve_details": "kubescape://vulnerability-manifests/{namespace}/{manifest_name}/cve_details/{cve_id}",
|
||||
}
|
||||
|
||||
content, _ := json.Marshal(result)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(content),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case "list_vulnerabilities_in_manifest":
|
||||
namespace, ok := arguments["namespace"]
|
||||
if !ok {
|
||||
namespace = "kubescape"
|
||||
}
|
||||
manifestName, ok := arguments["manifest_name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("manifest_name is required")
|
||||
}
|
||||
manifest, err := ksServer.ksClient.VulnerabilityManifests(namespace.(string)).Get(context.Background(), manifestName.(string), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vulnerability manifest: %s", err)
|
||||
}
|
||||
var cveList []v1beta1.Vulnerability
|
||||
for _, match := range manifest.Spec.Payload.Matches {
|
||||
cveList = append(cveList, match.Vulnerability)
|
||||
}
|
||||
responseJson, err := json.Marshal(cveList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cve list: %s", err)
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(responseJson),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case "list_vulnerability_matches_for_cve":
|
||||
namespace, ok := arguments["namespace"]
|
||||
if !ok {
|
||||
namespace = "kubescape"
|
||||
}
|
||||
manifestName, ok := arguments["manifest_name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("manifest_name is required")
|
||||
}
|
||||
cveID, ok := arguments["cve_id"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cve_id is required")
|
||||
}
|
||||
manifest, err := ksServer.ksClient.VulnerabilityManifests(namespace.(string)).Get(context.Background(), manifestName.(string), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vulnerability manifest: %s", err)
|
||||
}
|
||||
var match []v1beta1.Match
|
||||
for _, m := range manifest.Spec.Payload.Matches {
|
||||
if m.Vulnerability.ID == cveID.(string) {
|
||||
match = append(match, m)
|
||||
}
|
||||
}
|
||||
responseJson, err := json.Marshal(match)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cve details: %s", err)
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(responseJson),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case "list_configuration_security_scan_manifests":
|
||||
namespace, ok := arguments["namespace"]
|
||||
if !ok {
|
||||
namespace = "kubescape"
|
||||
}
|
||||
manifests, err := ksServer.ksClient.WorkloadConfigurationScans(namespace.(string)).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Found %d configuration manifests", len(manifests.Items))
|
||||
configManifests := []map[string]interface{}{}
|
||||
for _, manifest := range manifests.Items {
|
||||
item := map[string]interface{}{
|
||||
"namespace": manifest.Namespace,
|
||||
"manifest_name": manifest.Name,
|
||||
"resource_uri": fmt.Sprintf("kubescape://configuration-manifests/%s/%s", manifest.Namespace, manifest.Name),
|
||||
}
|
||||
configManifests = append(configManifests, item)
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"configuration_manifests": map[string]interface{}{
|
||||
"manifests": configManifests,
|
||||
},
|
||||
"available_templates": map[string]string{
|
||||
"configuration_manifest_details": "kubescape://configuration-manifests/{namespace}/{manifest_name}",
|
||||
},
|
||||
}
|
||||
content, _ := json.Marshal(result)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(content),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case "get_configuration_security_scan_manifest":
|
||||
namespace, ok := arguments["namespace"]
|
||||
if !ok {
|
||||
namespace = "kubescape"
|
||||
}
|
||||
manifestName, ok := arguments["manifest_name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("manifest_name is required")
|
||||
}
|
||||
manifest, err := ksServer.ksClient.WorkloadConfigurationScans(namespace.(string)).Get(context.Background(), manifestName.(string), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configuration manifest: %s", err)
|
||||
}
|
||||
responseJson, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configuration manifest: %s", err)
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(responseJson),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown tool: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func mcpServerEntrypoint() error {
|
||||
logger.L().Info("Starting MCP server...")
|
||||
|
||||
// Create a kubernetes client and verify it's working
|
||||
client, err := CreateKsObjectConnection("default", 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create kubernetes client: %v", err)
|
||||
}
|
||||
|
||||
// Create a new MCP server
|
||||
s := server.NewMCPServer(
|
||||
"Kubescape MCP Server",
|
||||
"0.0.1",
|
||||
server.WithToolCapabilities(false),
|
||||
server.WithRecovery(),
|
||||
)
|
||||
|
||||
ksServer := &KubescapeMcpserver{
|
||||
s: s,
|
||||
ksClient: client,
|
||||
}
|
||||
|
||||
// Creating Kubescape tools and resources
|
||||
|
||||
createVulnerabilityToolsAndResources(ksServer)
|
||||
createConfigurationsToolsAndResources(ksServer)
|
||||
|
||||
// Start the server
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
return fmt.Errorf("Server error: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMCPServerCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mcpserver",
|
||||
Short: "Start the Kubescape MCP server",
|
||||
Long: `Start the Kubescape MCP server`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return mcpServerEntrypoint()
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
14
cmd/mcpserver/storage.go
Normal file
14
cmd/mcpserver/storage.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/pkg/ksinit"
|
||||
|
||||
spdxv1beta1 "github.com/kubescape/storage/pkg/generated/clientset/versioned/typed/softwarecomposition/v1beta1"
|
||||
)
|
||||
|
||||
// CreateKsObjectConnection delegates to the shared ksinit package
|
||||
func CreateKsObjectConnection(namespace string, maxElapsedTime time.Duration) (spdxv1beta1.SpdxV1beta1Interface, error) {
|
||||
return ksinit.CreateKsObjectConnection(namespace, maxElapsedTime)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ The patch command can be run in 2 ways:
|
||||
| -a, --addr | Address of the buildkitd service | No | unix:///run/buildkit/buildkitd.sock |
|
||||
| -t, --tag | Tag of the resultant patched image | No | image_name-patched |
|
||||
| --timeout | Timeout for the patching process | No | 5m |
|
||||
| --ignore-errors| Ignore errors during patching | No | false |
|
||||
| -u, --username | Username for the image registry login | No | |
|
||||
| -p, --password | Password for the image registry login | No | |
|
||||
| -f, --format | Output file format. | No | |
|
||||
@@ -72,7 +73,7 @@ We will demonstrate how to use the patch command with an example of [nginx](http
|
||||
sudo buildkitd
|
||||
```
|
||||
|
||||
2. In a seperate terminal, run the `kubescape patch` command:
|
||||
2. In a separate terminal, run the `kubescape patch` command:
|
||||
|
||||
```bash
|
||||
sudo kubescape patch --image docker.io/library/nginx:1.22
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ref "github.com/distribution/distribution/reference"
|
||||
"github.com/docker/distribution/reference"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/kubescape/v3/pkg/imagescan"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -32,6 +27,7 @@ var patchCmdExamples = fmt.Sprintf(`
|
||||
func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
|
||||
var patchInfo metav1.PatchInfo
|
||||
var scanInfo cautils.ScanInfo
|
||||
var useDefaultMatchers bool
|
||||
|
||||
patchCmd := &cobra.Command{
|
||||
Use: "patch --image <image>:<tag> [flags]",
|
||||
@@ -53,12 +49,15 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := ks.Patch(context.Background(), &patchInfo, &scanInfo)
|
||||
// Set the UseDefaultMatchers field in scanInfo
|
||||
scanInfo.UseDefaultMatchers = useDefaultMatchers
|
||||
|
||||
exceedsSeverityThreshold, err := ks.Patch(&patchInfo, &scanInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if imagescan.ExceedsSeverityThreshold(results, imagescan.ParseSeverity(scanInfo.FailThresholdSeverity)) {
|
||||
if exceedsSeverityThreshold {
|
||||
shared.TerminateOnExceedingSeverity(&scanInfo, logger.L())
|
||||
}
|
||||
|
||||
@@ -70,6 +69,7 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
|
||||
patchCmd.PersistentFlags().StringVarP(&patchInfo.PatchedImageTag, "tag", "t", "", "Tag for the patched image. Defaults to '<image-tag>-patched' ")
|
||||
patchCmd.PersistentFlags().StringVarP(&patchInfo.BuildkitAddress, "address", "a", "unix:///run/buildkit/buildkitd.sock", "Address of buildkitd service, defaults to local buildkitd.sock")
|
||||
patchCmd.PersistentFlags().DurationVar(&patchInfo.Timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'")
|
||||
patchCmd.PersistentFlags().BoolVar(&patchInfo.IgnoreError, "ignore-errors", false, "Ignore errors and continue patching other images. Default to false")
|
||||
|
||||
patchCmd.PersistentFlags().StringVarP(&patchInfo.Username, "username", "u", "", "Username for registry login")
|
||||
patchCmd.PersistentFlags().StringVarP(&patchInfo.Password, "password", "p", "", "Password for registry login")
|
||||
@@ -79,6 +79,7 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
|
||||
patchCmd.PersistentFlags().BoolVarP(&scanInfo.VerboseMode, "verbose", "v", false, "Display full report. Default to false")
|
||||
|
||||
patchCmd.PersistentFlags().StringVarP(&scanInfo.FailThresholdSeverity, "severity-threshold", "s", "", "Severity threshold is the severity of a vulnerability at which the command fails and returns exit code 1")
|
||||
patchCmd.PersistentFlags().BoolVarP(&useDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false) for image scanning")
|
||||
|
||||
return patchCmd
|
||||
}
|
||||
@@ -97,22 +98,22 @@ func validateImagePatchInfo(patchInfo *metav1.PatchInfo) error {
|
||||
}
|
||||
|
||||
// Parse the image full name to get image name and tag
|
||||
named, err := ref.ParseNamed(patchInfoImage)
|
||||
named, err := reference.ParseNamed(patchInfoImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If no tag or digest is provided, default to 'latest'
|
||||
if ref.IsNameOnly(named) {
|
||||
if reference.IsNameOnly(named) {
|
||||
logger.L().Warning("Image name has no tag or digest, using latest as tag")
|
||||
named = ref.TagNameOnly(named)
|
||||
named = reference.TagNameOnly(named)
|
||||
}
|
||||
patchInfo.Image = named.String()
|
||||
|
||||
// If no patched image tag is provided, default to '<image-tag>-patched'
|
||||
if patchInfo.PatchedImageTag == "" {
|
||||
|
||||
taggedName, ok := named.(ref.Tagged)
|
||||
taggedName, ok := named.(reference.Tagged)
|
||||
if !ok {
|
||||
return errors.New("unexpected error while parsing image tag")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package patch
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/mocks"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -50,3 +52,18 @@ func TestGetPatchCmdWithNonExistentImage(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErrorMessage, err.Error())
|
||||
}
|
||||
|
||||
func Test_validateImagePatchInfo_EmptyImage(t *testing.T) {
|
||||
patchInfo := &metav1.PatchInfo{}
|
||||
err := validateImagePatchInfo(patchInfo)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "image tag is required", err.Error())
|
||||
}
|
||||
|
||||
func Test_validateImagePatchInfo_Image(t *testing.T) {
|
||||
patchInfo := &metav1.PatchInfo{
|
||||
Image: "testing",
|
||||
}
|
||||
err := validateImagePatchInfo(patchInfo)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
51
cmd/prerequisites/prerequisites.go
Normal file
51
cmd/prerequisites/prerequisites.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package prerequisites
|
||||
|
||||
import (
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
"github.com/kubescape/sizing-checker/pkg/checks/connectivitycheck"
|
||||
"github.com/kubescape/sizing-checker/pkg/checks/ebpfcheck"
|
||||
"github.com/kubescape/sizing-checker/pkg/checks/pvcheck"
|
||||
"github.com/kubescape/sizing-checker/pkg/checks/sizing"
|
||||
"github.com/kubescape/sizing-checker/pkg/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func GetPreReqCmd(ks meta.IKubescape) *cobra.Command {
|
||||
var kubeconfigPath *string
|
||||
|
||||
// preReqCmd represents the prerequisites command
|
||||
preReqCmd := &cobra.Command{
|
||||
Use: "prerequisites",
|
||||
Short: "Check prerequisites for installing Kubescape Operator",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
clientSet, inCluster := common.BuildKubeClient(*kubeconfigPath)
|
||||
if clientSet == nil {
|
||||
logger.L().Fatal("Could not create kube client. Exiting.")
|
||||
}
|
||||
|
||||
// 1) Collect cluster data
|
||||
clusterData, err := common.CollectClusterData(ks.Context(), clientSet)
|
||||
if err != nil {
|
||||
logger.L().Error("Failed to collect cluster data", helpers.Error(err))
|
||||
}
|
||||
|
||||
// 2) Run checks
|
||||
sizingResult := sizing.RunSizingChecker(clusterData)
|
||||
pvResult := pvcheck.RunPVProvisioningCheck(ks.Context(), clientSet, clusterData, inCluster)
|
||||
connectivityResult := connectivitycheck.RunConnectivityChecks(ks.Context(), clientSet, clusterData, inCluster)
|
||||
ebpfResult := ebpfcheck.RunEbpfCheck(ks.Context(), clientSet, clusterData, inCluster)
|
||||
|
||||
// 3) Build and export the final ReportData
|
||||
finalReport := common.BuildReportData(clusterData, sizingResult, pvResult, connectivityResult, ebpfResult)
|
||||
finalReport.InCluster = inCluster
|
||||
|
||||
common.GenerateOutput(finalReport, inCluster)
|
||||
},
|
||||
}
|
||||
|
||||
kubeconfigPath = preReqCmd.PersistentFlags().String("kubeconfig", "", "Path to the kubeconfig file. If not set, in-cluster config is used or $HOME/.kube/config if outside a cluster.")
|
||||
|
||||
return preReqCmd
|
||||
}
|
||||
26
cmd/root.go
26
cmd/root.go
@@ -1,10 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
"github.com/kubescape/kubescape/v3/cmd/completion"
|
||||
@@ -12,16 +13,18 @@ import (
|
||||
"github.com/kubescape/kubescape/v3/cmd/download"
|
||||
"github.com/kubescape/kubescape/v3/cmd/fix"
|
||||
"github.com/kubescape/kubescape/v3/cmd/list"
|
||||
"github.com/kubescape/kubescape/v3/cmd/mcpserver"
|
||||
"github.com/kubescape/kubescape/v3/cmd/operator"
|
||||
"github.com/kubescape/kubescape/v3/cmd/patch"
|
||||
"github.com/kubescape/kubescape/v3/cmd/prerequisites"
|
||||
"github.com/kubescape/kubescape/v3/cmd/scan"
|
||||
"github.com/kubescape/kubescape/v3/cmd/update"
|
||||
"github.com/kubescape/kubescape/v3/cmd/vap"
|
||||
"github.com/kubescape/kubescape/v3/cmd/version"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
"github.com/kubescape/kubescape/v3/core/core"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -41,8 +44,8 @@ var ksExamples = fmt.Sprintf(`
|
||||
%[1]s config view
|
||||
`, cautils.ExecName())
|
||||
|
||||
func NewDefaultKubescapeCommand() *cobra.Command {
|
||||
ks := core.NewKubescape()
|
||||
func NewDefaultKubescapeCommand(ctx context.Context) *cobra.Command {
|
||||
ks := core.NewKubescape(ctx)
|
||||
return getRootCmd(ks)
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "kubescape",
|
||||
Short: "Kubescape is a tool for testing Kubernetes security posture. Docs: https://hub.armosec.io/docs",
|
||||
Short: "Kubescape is a tool for testing Kubernetes security posture. Docs: https://kubescape.io/docs/",
|
||||
Example: ksExamples,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
k8sinterface.SetClusterContextName(rootInfo.KubeContext)
|
||||
@@ -83,8 +86,6 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&rootInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/")))
|
||||
rootCmd.PersistentFlags().StringVar(&rootInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]")
|
||||
rootCmd.PersistentFlags().BoolVarP(&rootInfo.DisableColor, "disable-color", "", false, "Disable color output for logging")
|
||||
rootCmd.PersistentFlags().BoolVarP(&rootInfo.EnableColor, "enable-color", "", false, "Force enable color output for logging")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&rootInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context")
|
||||
// Supported commands
|
||||
@@ -92,12 +93,15 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
rootCmd.AddCommand(download.GetDownloadCmd(ks))
|
||||
rootCmd.AddCommand(list.GetListCmd(ks))
|
||||
rootCmd.AddCommand(completion.GetCompletionCmd())
|
||||
rootCmd.AddCommand(version.GetVersionCmd())
|
||||
rootCmd.AddCommand(version.GetVersionCmd(ks))
|
||||
rootCmd.AddCommand(config.GetConfigCmd(ks))
|
||||
rootCmd.AddCommand(update.GetUpdateCmd())
|
||||
rootCmd.AddCommand(update.GetUpdateCmd(ks))
|
||||
rootCmd.AddCommand(fix.GetFixCmd(ks))
|
||||
rootCmd.AddCommand(patch.GetPatchCmd(ks))
|
||||
rootCmd.AddCommand(vap.GetVapHelperCmd())
|
||||
rootCmd.AddCommand(operator.GetOperatorCmd(ks))
|
||||
rootCmd.AddCommand(prerequisites.GetPreReqCmd(ks))
|
||||
rootCmd.AddCommand(mcpserver.GetMCPServerCmd())
|
||||
|
||||
// deprecated commands
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
@@ -112,7 +116,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
ks := NewDefaultKubescapeCommand()
|
||||
func Execute(ctx context.Context) error {
|
||||
ks := NewDefaultKubescapeCommand(ctx)
|
||||
return ks.Execute()
|
||||
}
|
||||
|
||||
24
cmd/root_test.go
Normal file
24
cmd/root_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDefaultKubescapeCommand(t *testing.T) {
|
||||
t.Run("NewDefaultKubescapeCommand", func(t *testing.T) {
|
||||
cmd := NewDefaultKubescapeCommand(context.Background())
|
||||
assert.NotNil(t, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
t.Run("Execute", func(t *testing.T) {
|
||||
err := Execute(context.Background())
|
||||
if err != nil {
|
||||
assert.EqualErrorf(t, err, "unknown command \"^\\\\QTestExecute\\\\E$\" for \"kubescape\"", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -8,20 +8,16 @@ import (
|
||||
v1 "github.com/kubescape/backend/pkg/client/v1"
|
||||
"github.com/kubescape/backend/pkg/servicediscovery"
|
||||
sdClientV2 "github.com/kubescape/backend/pkg/servicediscovery/v2"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/go-logger/iconlogger"
|
||||
"github.com/kubescape/go-logger/zaplogger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
func initLogger() {
|
||||
logger.DisableColor(rootInfo.DisableColor)
|
||||
logger.EnableColor(rootInfo.EnableColor)
|
||||
|
||||
if rootInfo.LoggerName == "" {
|
||||
if l := os.Getenv("KS_LOGGER_NAME"); l != "" {
|
||||
rootInfo.LoggerName = l
|
||||
@@ -35,8 +31,8 @@ func initLogger() {
|
||||
}
|
||||
|
||||
logger.InitLogger(rootInfo.LoggerName)
|
||||
|
||||
}
|
||||
|
||||
func initLoggerLevel() {
|
||||
if rootInfo.Logger == helpers.InfoLevel.String() {
|
||||
} else if l := os.Getenv("KS_LOGGER"); l != "" {
|
||||
@@ -77,7 +73,7 @@ func initEnvironment() {
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.L().Fatal("failed to to get services from server", helpers.Error(err), helpers.String("server", rootInfo.DiscoveryServerURL))
|
||||
logger.L().Fatal("failed to get services from server", helpers.Error(err), helpers.String("server", rootInfo.DiscoveryServerURL))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -32,7 +29,7 @@ var (
|
||||
Run '%[1]s list controls' for the list of supported controls
|
||||
|
||||
Control documentation:
|
||||
https://hub.armosec.io/docs/controls
|
||||
https://kubescape.io/docs/controls/
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
@@ -98,12 +95,11 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
results, err := ks.Scan(ctx, scanInfo)
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
if err := results.HandleResults(ctx); err != nil {
|
||||
if err := results.HandleResults(ks.Context(), scanInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
if !scanInfo.VerboseMode {
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
reporthandlingapis "github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
reporthandlingapis "github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -115,13 +112,12 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm
|
||||
|
||||
scanInfo.SetPolicyIdentifiers(frameworks, apisv1.KindFramework)
|
||||
|
||||
ctx := context.TODO()
|
||||
results, err := ks.Scan(ctx, scanInfo)
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ctx); err != nil {
|
||||
if err = results.HandleResults(ks.Context(), scanInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/kubescape/v3/pkg/imagescan"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -25,12 +22,18 @@ var (
|
||||
# Scan the 'nginx' image and see the full report
|
||||
%[1]s scan image "nginx" -v
|
||||
|
||||
# Scan the 'nginx' image and use exceptions
|
||||
%[1]s scan image "nginx" --exceptions exceptions.json
|
||||
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
// getImageCmd returns the scan image command
|
||||
func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command {
|
||||
var imgCredentials shared.ImageCredentials
|
||||
var exceptions string
|
||||
var useDefaultMatchers bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "image <image>:<tag> [flags]",
|
||||
Short: "Scan an image for vulnerabilities",
|
||||
@@ -51,17 +54,19 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
|
||||
}
|
||||
|
||||
imgScanInfo := &metav1.ImageScanInfo{
|
||||
Image: args[0],
|
||||
Username: imgCredentials.Username,
|
||||
Password: imgCredentials.Password,
|
||||
Image: args[0],
|
||||
Username: imgCredentials.Username,
|
||||
Password: imgCredentials.Password,
|
||||
Exceptions: exceptions,
|
||||
UseDefaultMatchers: useDefaultMatchers,
|
||||
}
|
||||
|
||||
results, err := ks.ScanImage(context.Background(), imgScanInfo, scanInfo)
|
||||
exceedsSeverityThreshold, err := ks.ScanImage(imgScanInfo, scanInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if imagescan.ExceedsSeverityThreshold(results, imagescan.ParseSeverity(scanInfo.FailThresholdSeverity)) {
|
||||
if exceedsSeverityThreshold {
|
||||
shared.TerminateOnExceedingSeverity(scanInfo, logger.L())
|
||||
}
|
||||
|
||||
@@ -69,8 +74,11 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
|
||||
},
|
||||
}
|
||||
|
||||
// The exceptions flag
|
||||
cmd.PersistentFlags().StringVarP(&exceptions, "exceptions", "", "", "Path to the exceptions file")
|
||||
cmd.PersistentFlags().StringVarP(&imgCredentials.Username, "username", "u", "", "Username for registry login")
|
||||
cmd.PersistentFlags().StringVarP(&imgCredentials.Password, "password", "p", "", "Password for registry login")
|
||||
cmd.PersistentFlags().BoolVarP(&useDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
@@ -15,12 +14,12 @@ import (
|
||||
)
|
||||
|
||||
var scanCmdExamples = fmt.Sprintf(`
|
||||
Scan command is for scanning an existing cluster or kubernetes manifest files based on pre-defined frameworks
|
||||
|
||||
Scan command is for scanning an existing cluster or kubernetes manifest files based on pre-defined frameworks
|
||||
|
||||
# Scan current cluster
|
||||
%[1]s scan
|
||||
|
||||
# Scan kubernetes manifest files
|
||||
# Scan kubernetes manifest files
|
||||
%[1]s scan .
|
||||
|
||||
# Scan and save the results in the JSON format
|
||||
@@ -29,7 +28,7 @@ var scanCmdExamples = fmt.Sprintf(`
|
||||
# Display all resources
|
||||
%[1]s scan --verbose
|
||||
|
||||
# Scan different clusters from the kubectl context
|
||||
# Scan different clusters from the kubectl context
|
||||
%[1]s scan --kube-context <kubernetes context>
|
||||
`, cautils.ExecName())
|
||||
|
||||
@@ -64,6 +63,8 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
scanInfo.TriggeredByCLI = true
|
||||
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.AccountID, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
|
||||
scanCmd.PersistentFlags().StringVarP(&scanInfo.AccessKey, "access-key", "", "", "Kubescape SaaS access key. Default will load access key from cache")
|
||||
scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal")
|
||||
@@ -89,7 +90,10 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Submit the scan results to Kubescape SaaS where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted")
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.OmitRawResources, "omit-raw-resources", "", false, "Omit raw resources from the output. By default the raw resources are included in the output")
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.PrintAttackTree, "print-attack-tree", "", false, "Print attack tree")
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.EnableRegoPrint, "enable-rego-prints", "", false, "Enable sending to rego prints to the logs (use with debug log level: -l debug)")
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.ScanImages, "scan-images", "", false, "Scan resources images")
|
||||
scanCmd.PersistentFlags().BoolVarP(&scanInfo.UseDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false) for image scanning")
|
||||
scanCmd.PersistentFlags().StringSliceVar(&scanInfo.LabelsToCopy, "labels-to-copy", nil, "Labels to copy from workloads to scan reports for easy identification. e.g: --labels-to-copy=app,team,environment")
|
||||
|
||||
scanCmd.PersistentFlags().MarkDeprecated("fail-threshold", "use '--compliance-threshold' flag instead. Flag will be removed at 1.Dec.2023")
|
||||
scanCmd.PersistentFlags().MarkDeprecated("create-account", "Create account is no longer supported. In case of a missing Account ID and a configured backend server, a new account id will be generated automatically by Kubescape. Feel free to contact the Kubescape maintainers for more information.")
|
||||
@@ -132,15 +136,12 @@ func setSecurityViewScanInfo(args []string, scanInfo *cautils.ScanInfo) {
|
||||
}
|
||||
|
||||
func securityScan(scanInfo cautils.ScanInfo, ks meta.IKubescape) error {
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
results, err := ks.Scan(ctx, &scanInfo)
|
||||
results, err := ks.Scan(&scanInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ctx); err != nil {
|
||||
if err = results.HandleResults(ks.Context(), &scanInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,19 @@ package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/mocks"
|
||||
v1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExceedsSeverity(t *testing.T) {
|
||||
@@ -188,20 +187,23 @@ type spyLogger struct {
|
||||
setItems []spyLogMessage
|
||||
}
|
||||
|
||||
func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) SetLevel(level string) error { return nil }
|
||||
func (l *spyLogger) GetLevel() string { return "" }
|
||||
func (l *spyLogger) SetWriter(w *os.File) {}
|
||||
func (l *spyLogger) GetWriter() *os.File { return &os.File{} }
|
||||
func (l *spyLogger) LoggerName() string { return "" }
|
||||
func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l }
|
||||
func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {}
|
||||
var _ helpers.ILogger = &spyLogger{}
|
||||
|
||||
func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) SetLevel(level string) error { return nil }
|
||||
func (l *spyLogger) GetLevel() string { return "" }
|
||||
func (l *spyLogger) SetWriter(w *os.File) {}
|
||||
func (l *spyLogger) GetWriter() *os.File { return &os.File{} }
|
||||
func (l *spyLogger) LoggerName() string { return "" }
|
||||
func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l }
|
||||
func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) TimedWrapper(funcName string, timeout time.Duration, task func()) {}
|
||||
|
||||
func (l *spyLogger) Fatal(msg string, details ...helpers.IDetails) {
|
||||
firstDetail := details[0]
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
v1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -67,13 +65,12 @@ func getWorkloadCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comma
|
||||
setWorkloadScanInfo(scanInfo, kind, name)
|
||||
|
||||
// todo: add api version if provided
|
||||
ctx := context.TODO()
|
||||
results, err := ks.Scan(ctx, scanInfo)
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ctx); err != nil {
|
||||
if err = results.HandleResults(ks.Context(), scanInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -94,3 +94,17 @@ func TestGetWorkloadCmd_ChartPathAndFilePathEmpty(t *testing.T) {
|
||||
expectedErrorMessage = "invalid workload identifier"
|
||||
assert.Equal(t, expectedErrorMessage, err.Error())
|
||||
}
|
||||
|
||||
func Test_parseWorkloadIdentifierString_Empty(t *testing.T) {
|
||||
t.Run("empty identifier", func(t *testing.T) {
|
||||
_, _, err := parseWorkloadIdentifierString("")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_parseWorkloadIdentifierString_NoError(t *testing.T) {
|
||||
t.Run("valid identifier", func(t *testing.T) {
|
||||
_, _, err := parseWorkloadIdentifierString("default/Deployment")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
@@ -20,20 +21,23 @@ type spyLogger struct {
|
||||
setItems []spyLogMessage
|
||||
}
|
||||
|
||||
func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) SetLevel(level string) error { return nil }
|
||||
func (l *spyLogger) GetLevel() string { return "" }
|
||||
func (l *spyLogger) SetWriter(w *os.File) {}
|
||||
func (l *spyLogger) GetWriter() *os.File { return &os.File{} }
|
||||
func (l *spyLogger) LoggerName() string { return "" }
|
||||
func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l }
|
||||
func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {}
|
||||
var _ helpers.ILogger = &spyLogger{}
|
||||
|
||||
func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) SetLevel(level string) error { return nil }
|
||||
func (l *spyLogger) GetLevel() string { return "" }
|
||||
func (l *spyLogger) SetWriter(w *os.File) {}
|
||||
func (l *spyLogger) GetWriter() *os.File { return &os.File{} }
|
||||
func (l *spyLogger) LoggerName() string { return "" }
|
||||
func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l }
|
||||
func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {}
|
||||
func (l *spyLogger) TimedWrapper(funcName string, timeout time.Duration, task func()) {}
|
||||
|
||||
func (l *spyLogger) Fatal(msg string, details ...helpers.IDetails) {
|
||||
firstDetail := details[0]
|
||||
|
||||
@@ -5,11 +5,13 @@ package update
|
||||
// kubescape update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -24,30 +26,31 @@ var updateCmdExamples = fmt.Sprintf(`
|
||||
%[1]s update
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetUpdateCmd() *cobra.Command {
|
||||
func GetUpdateCmd(ks meta.IKubescape) *cobra.Command {
|
||||
updateCmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update to latest release version",
|
||||
Long: ``,
|
||||
Example: updateCmdExamples,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
ctx := context.TODO()
|
||||
v := cautils.NewVersionCheckHandler()
|
||||
versionCheckRequest := cautils.NewVersionCheckRequest(cautils.BuildNumber, "", "", "update")
|
||||
v.CheckLatestVersion(ctx, versionCheckRequest)
|
||||
v := versioncheck.NewVersionCheckHandler()
|
||||
versionCheckRequest := versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "update", nil)
|
||||
if err := v.CheckLatestVersion(ks.Context(), versionCheckRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Checking the user's version of kubescape to the latest release
|
||||
if cautils.BuildNumber == "" || strings.Contains(cautils.BuildNumber, "rc") {
|
||||
if versioncheck.BuildNumber == "" || strings.Contains(versioncheck.BuildNumber, "rc") {
|
||||
//your version is unknown
|
||||
fmt.Printf("Nothing to update: you are running the development version\n")
|
||||
} else if cautils.LatestReleaseVersion == "" {
|
||||
} else if versioncheck.LatestReleaseVersion == "" {
|
||||
//Failed to check for updates
|
||||
logger.L().Info(("Failed to check for updates"))
|
||||
} else if cautils.BuildNumber == cautils.LatestReleaseVersion {
|
||||
logger.L().Info("Failed to check for updates")
|
||||
} else if versioncheck.BuildNumber == versioncheck.LatestReleaseVersion {
|
||||
//your version == latest version
|
||||
logger.L().Info(("Nothing to update: you are running the latest version"), helpers.String("Version", cautils.BuildNumber))
|
||||
logger.L().Info("Nothing to update: you are running the latest version", helpers.String("Version", versioncheck.BuildNumber))
|
||||
} else {
|
||||
fmt.Printf("Version %s is available. Please refer to our installation documentation: %s\n", cautils.LatestReleaseVersion, installationLink)
|
||||
fmt.Printf("Version %s is available. Please refer to our installation documentation: %s\n", versioncheck.LatestReleaseVersion, installationLink)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
18
cmd/update/update_test.go
Normal file
18
cmd/update/update_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetUpdateCmd(t *testing.T) {
|
||||
ks := core.NewKubescape(context.TODO())
|
||||
cmd := GetUpdateCmd(ks)
|
||||
assert.NotNil(t, cmd)
|
||||
|
||||
err := cmd.RunE(cmd, []string{})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
237
cmd/vap/vap.go
Normal file
237
cmd/vap/vap.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package vap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
admissionv1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var vapHelperCmdExamples = fmt.Sprintf(`
|
||||
vap command can be used for managing Validating Admission Policies in a Kubernetes cluster.
|
||||
This is an experimental feature and it might change.
|
||||
|
||||
Examples:
|
||||
|
||||
# Install Kubescape CEL admission policy library
|
||||
%[1]s vap deploy-library | kubectl apply -f -
|
||||
# Create a policy binding
|
||||
%[1]s vap create-policy-binding --name my-policy-binding --policy c-0016 --namespace=my-namespace | kubectl apply -f -
|
||||
`, cautils.ExecName())
|
||||
|
||||
func GetVapHelperCmd() *cobra.Command {
|
||||
|
||||
vapHelperCmd := &cobra.Command{
|
||||
Use: "vap",
|
||||
Short: "Helper commands for managing Validating Admission Policies in a Kubernetes cluster",
|
||||
Long: ``,
|
||||
Example: vapHelperCmdExamples,
|
||||
}
|
||||
|
||||
// Create subcommands
|
||||
vapHelperCmd.AddCommand(getDeployLibraryCmd())
|
||||
vapHelperCmd.AddCommand(getCreatePolicyBindingCmd())
|
||||
|
||||
return vapHelperCmd
|
||||
}
|
||||
|
||||
func getDeployLibraryCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "deploy-library",
|
||||
Short: "Install Kubescape CEL admission policy library",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return deployLibrary()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCreatePolicyBindingCmd() *cobra.Command {
|
||||
var policyBindingName string
|
||||
var policyName string
|
||||
var namespaceArr []string
|
||||
var labelArr []string
|
||||
var action string
|
||||
var parameterReference string
|
||||
|
||||
createPolicyBindingCmd := &cobra.Command{
|
||||
Use: "create-policy-binding",
|
||||
Short: "Create a policy binding",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Validate the inputs
|
||||
if err := isValidK8sObjectName(policyBindingName); err != nil {
|
||||
return fmt.Errorf("invalid policy binding name %s: %w", policyBindingName, err)
|
||||
}
|
||||
if err := isValidK8sObjectName(policyName); err != nil {
|
||||
return fmt.Errorf("invalid policy name %s: %w", policyName, err)
|
||||
}
|
||||
for _, namespace := range namespaceArr {
|
||||
if err := isValidK8sObjectName(namespace); err != nil {
|
||||
return fmt.Errorf("invalid namespace %s: %w", namespace, err)
|
||||
}
|
||||
}
|
||||
for _, label := range labelArr {
|
||||
// Label selector must be in the format key=value
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9]+=[a-zA-Z0-9]+$`).MatchString(label) {
|
||||
return fmt.Errorf("invalid label selector: %s", label)
|
||||
}
|
||||
}
|
||||
if action != "Deny" && action != "Audit" && action != "Warn" {
|
||||
return fmt.Errorf("invalid action: %s", action)
|
||||
}
|
||||
if parameterReference != "" {
|
||||
if err := isValidK8sObjectName(parameterReference); err != nil {
|
||||
return fmt.Errorf("invalid parameter reference %s: %w", parameterReference, err)
|
||||
}
|
||||
}
|
||||
|
||||
return createPolicyBinding(policyBindingName, policyName, action, parameterReference, namespaceArr, labelArr)
|
||||
},
|
||||
}
|
||||
// Must specify the name of the policy binding
|
||||
createPolicyBindingCmd.Flags().StringVarP(&policyBindingName, "name", "n", "", "Name of the policy binding")
|
||||
createPolicyBindingCmd.MarkFlagRequired("name")
|
||||
createPolicyBindingCmd.Flags().StringVarP(&policyName, "policy", "p", "", "Name of the policy to bind the resources to")
|
||||
createPolicyBindingCmd.MarkFlagRequired("policy")
|
||||
createPolicyBindingCmd.Flags().StringSliceVar(&namespaceArr, "namespace", []string{}, "Resource namespace selector")
|
||||
createPolicyBindingCmd.Flags().StringSliceVar(&labelArr, "label", []string{}, "Resource label selector")
|
||||
createPolicyBindingCmd.Flags().StringVarP(&action, "action", "a", "Deny", "Action to take when policy fails")
|
||||
createPolicyBindingCmd.Flags().StringVarP(¶meterReference, "parameter-reference", "r", "", "Parameter reference object name")
|
||||
|
||||
return createPolicyBindingCmd
|
||||
}
|
||||
|
||||
// Implementation of the VAP helper commands
|
||||
// deploy-library
|
||||
func deployLibrary() error {
|
||||
logger.L().Info("Downloading the Kubescape CEL admission policy library")
|
||||
// Download the policy-configuration-definition.yaml from the latest release URL
|
||||
policyConfigurationDefinitionURL := "https://github.com/kubescape/cel-admission-library/releases/latest/download/policy-configuration-definition.yaml"
|
||||
policyConfigurationDefinition, err := downloadFileToString(policyConfigurationDefinitionURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Download the basic-control-configuration.yaml from the latest release URL
|
||||
basicControlConfigurationURL := "https://github.com/kubescape/cel-admission-library/releases/latest/download/basic-control-configuration.yaml"
|
||||
basicControlConfiguration, err := downloadFileToString(basicControlConfigurationURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Download the kubescape-validating-admission-policies.yaml from the latest release URL
|
||||
kubescapeValidatingAdmissionPoliciesURL := "https://github.com/kubescape/cel-admission-library/releases/latest/download/kubescape-validating-admission-policies.yaml"
|
||||
kubescapeValidatingAdmissionPolicies, err := downloadFileToString(kubescapeValidatingAdmissionPoliciesURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.L().Info("Successfully downloaded admission policy library")
|
||||
|
||||
// Print the downloaded files to the STDOUT for the user to apply connecting them to a single YAML with ---
|
||||
fmt.Println(policyConfigurationDefinition)
|
||||
fmt.Println("---")
|
||||
fmt.Println(basicControlConfiguration)
|
||||
fmt.Println("---")
|
||||
fmt.Println(kubescapeValidatingAdmissionPolicies)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadFileToString(url string) (string, error) {
|
||||
// Send an HTTP GET request to the URL
|
||||
response, err := http.Get(url) //nolint:gosec
|
||||
if err != nil {
|
||||
return "", err // Return an empty string and the error if the request fails
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Check for a successful response (HTTP 200 OK)
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to download file: %s", response.Status)
|
||||
}
|
||||
|
||||
// Read the response body
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err // Return an empty string and the error if reading fails
|
||||
}
|
||||
|
||||
// Convert the byte slice to a string
|
||||
bodyString := string(bodyBytes)
|
||||
return bodyString, nil
|
||||
}
|
||||
|
||||
func isValidK8sObjectName(name string) error {
|
||||
// Kubernetes object names must consist of lower case alphanumeric characters, '-' or '.',
|
||||
// and must start and end with an alphanumeric character (e.g., 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')
|
||||
// Max length of 63 characters.
|
||||
if len(name) > 63 {
|
||||
return errors.New("name should be less than 63 characters")
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
|
||||
if !regex.MatchString(name) {
|
||||
return errors.New("name should consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a policy binding
|
||||
func createPolicyBinding(bindingName string, policyName string, action string, paramRefName string, namespaceArr []string, labelMatch []string) error {
|
||||
// Create a policy binding struct
|
||||
policyBinding := &admissionv1.ValidatingAdmissionPolicyBinding{}
|
||||
// Print the policy binding after marshalling it to YAML to the STDOUT
|
||||
// The user can apply the output to the cluster
|
||||
policyBinding.APIVersion = "admissionregistration.k8s.io/v1"
|
||||
policyBinding.Name = bindingName
|
||||
policyBinding.Kind = "ValidatingAdmissionPolicyBinding"
|
||||
policyBinding.Spec.PolicyName = policyName
|
||||
policyBinding.Spec.MatchResources = &admissionv1.MatchResources{}
|
||||
if len(namespaceArr) > 0 {
|
||||
policyBinding.Spec.MatchResources.NamespaceSelector = &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "kubernetes.io/metadata.name",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: namespaceArr,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(labelMatch) > 0 {
|
||||
policyBinding.Spec.MatchResources.ObjectSelector = &metav1.LabelSelector{}
|
||||
policyBinding.Spec.MatchResources.ObjectSelector.MatchLabels = make(map[string]string)
|
||||
for _, label := range labelMatch {
|
||||
labelParts := regexp.MustCompile(`=`).Split(label, 2)
|
||||
policyBinding.Spec.MatchResources.ObjectSelector.MatchLabels[labelParts[0]] = labelParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
policyBinding.Spec.ValidationActions = []admissionv1.ValidationAction{admissionv1.ValidationAction(action)}
|
||||
paramAction := admissionv1.DenyAction
|
||||
if paramRefName != "" {
|
||||
policyBinding.Spec.ParamRef = &admissionv1.ParamRef{
|
||||
Name: paramRefName,
|
||||
ParameterNotFoundAction: ¶mAction,
|
||||
}
|
||||
}
|
||||
// Marshal the policy binding to YAML
|
||||
out, err := yaml.Marshal(policyBinding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(out))
|
||||
return nil
|
||||
}
|
||||
10
cmd/vap/vap_test.go
Normal file
10
cmd/vap/vap_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package vap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetVapHelperCmd(t *testing.T) {
|
||||
// Call the GetFixCmd function
|
||||
_ = GetVapHelperCmd()
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/meta"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func GetVersionCmd() *cobra.Command {
|
||||
func GetVersionCmd(ks meta.IKubescape) *cobra.Command {
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get current version",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.TODO()
|
||||
v := cautils.NewIVersionCheckHandler(ctx)
|
||||
versionCheckRequest := cautils.NewVersionCheckRequest(cautils.BuildNumber, "", "", "version")
|
||||
v.CheckLatestVersion(ctx, versionCheckRequest)
|
||||
fmt.Fprintf(cmd.OutOrStdout(),
|
||||
v := versioncheck.NewIVersionCheckHandler(ks.Context())
|
||||
_ = v.CheckLatestVersion(ks.Context(), versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "version", nil))
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
|
||||
"Your current version is: %s\n",
|
||||
versionCheckRequest.ClientVersion,
|
||||
versioncheck.BuildNumber,
|
||||
)
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -2,10 +2,13 @@ package version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/core"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -17,7 +20,7 @@ func TestGetVersionCmd(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Undefined Build Number",
|
||||
buildNumber: "",
|
||||
buildNumber: "unknown",
|
||||
want: "Your current version is: unknown\n",
|
||||
},
|
||||
{
|
||||
@@ -28,9 +31,10 @@ func TestGetVersionCmd(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cautils.BuildNumber = tt.buildNumber
|
||||
versioncheck.BuildNumber = tt.buildNumber
|
||||
|
||||
if cmd := GetVersionCmd(); cmd != nil {
|
||||
ks := core.NewKubescape(context.TODO())
|
||||
if cmd := GetVersionCmd(ks); cmd != nil {
|
||||
buf := bytes.NewBufferString("")
|
||||
cmd.SetOut(buf)
|
||||
cmd.Execute()
|
||||
|
||||
250
core/README.md
250
core/README.md
@@ -1,14 +1,248 @@
|
||||
# Kubescape core package
|
||||
# Kubescape Core Package
|
||||
|
||||
The `core` package provides the main Kubescape scanning engine as a Go library, allowing you to integrate Kubescape security scanning directly into your applications.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [API Reference](#api-reference)
|
||||
- [Examples](#examples)
|
||||
- [Configuration Options](#configuration-options)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/kubescape/kubescape/v3/core
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
// initialize kubescape
|
||||
ks := core.NewKubescape()
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
// scan cluster
|
||||
results, err := ks.Scan(&cautils.ScanInfo{})
|
||||
"github.com/kubescape/kubescape/v3/core"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
)
|
||||
|
||||
// convert scan results to json
|
||||
jsonRes, err := results.ToJson()
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
```
|
||||
// Initialize Kubescape
|
||||
ks := core.NewKubescape(ctx)
|
||||
|
||||
// Configure scan
|
||||
scanInfo := &cautils.ScanInfo{
|
||||
// Scan the current cluster
|
||||
ScanAll: true,
|
||||
}
|
||||
|
||||
// Run scan
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("Scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Convert results to JSON
|
||||
jsonRes, err := results.ToJson()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to convert results: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(jsonRes))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Creating a Kubescape Instance
|
||||
|
||||
```go
|
||||
// Create with context
|
||||
ks := core.NewKubescape(ctx)
|
||||
```
|
||||
|
||||
### Scanning
|
||||
|
||||
```go
|
||||
// Scan with configuration
|
||||
results, err := ks.Scan(scanInfo)
|
||||
```
|
||||
|
||||
### Listing Frameworks and Controls
|
||||
|
||||
```go
|
||||
// List available policies
|
||||
err := ks.List(listPolicies)
|
||||
```
|
||||
|
||||
### Downloading Artifacts
|
||||
|
||||
```go
|
||||
// Download for offline use
|
||||
err := ks.Download(downloadInfo)
|
||||
```
|
||||
|
||||
### Image Scanning
|
||||
|
||||
```go
|
||||
// Scan container image
|
||||
exceedsSeverity, err := ks.ScanImage(imgScanInfo, scanInfo)
|
||||
```
|
||||
|
||||
### Fixing Misconfigurations
|
||||
|
||||
```go
|
||||
// Apply fixes to manifests
|
||||
err := ks.Fix(fixInfo)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Scan a Specific Framework
|
||||
|
||||
```go
|
||||
scanInfo := &cautils.ScanInfo{}
|
||||
scanInfo.SetPolicyIdentifiers([]string{"nsa"}, "framework")
|
||||
|
||||
results, err := ks.Scan(scanInfo)
|
||||
```
|
||||
|
||||
### Scan Specific Namespaces
|
||||
|
||||
```go
|
||||
scanInfo := &cautils.ScanInfo{
|
||||
IncludeNamespaces: "production,staging",
|
||||
}
|
||||
|
||||
results, err := ks.Scan(scanInfo)
|
||||
```
|
||||
|
||||
### Scan Local YAML Files
|
||||
|
||||
```go
|
||||
scanInfo := &cautils.ScanInfo{
|
||||
InputPatterns: []string{"/path/to/manifests"},
|
||||
}
|
||||
scanInfo.SetScanType(cautils.ScanTypeRepo)
|
||||
|
||||
results, err := ks.Scan(scanInfo)
|
||||
```
|
||||
|
||||
### Export Results to Different Formats
|
||||
|
||||
```go
|
||||
results, _ := ks.Scan(scanInfo)
|
||||
|
||||
// JSON
|
||||
jsonData, _ := results.ToJson()
|
||||
|
||||
// Get summary
|
||||
summary := results.GetData().Report.SummaryDetails
|
||||
fmt.Printf("Compliance Score: %.2f%%\n", summary.ComplianceScore)
|
||||
```
|
||||
|
||||
### Scan with Compliance Threshold
|
||||
|
||||
```go
|
||||
scanInfo := &cautils.ScanInfo{
|
||||
ComplianceThreshold: 80.0, // Fail if below 80%
|
||||
}
|
||||
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
// Handle scan failure
|
||||
}
|
||||
|
||||
// Check if threshold was exceeded
|
||||
if results.GetData().Report.SummaryDetails.ComplianceScore < scanInfo.ComplianceThreshold {
|
||||
log.Fatal("Compliance score below threshold")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### ScanInfo Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `AccountID` | string | Kubescape SaaS account ID |
|
||||
| `AccessKey` | string | Kubescape SaaS access key |
|
||||
| `InputPatterns` | []string | Paths to scan (files, directories, URLs) |
|
||||
| `ExcludedNamespaces` | string | Comma-separated namespaces to exclude |
|
||||
| `IncludeNamespaces` | string | Comma-separated namespaces to include |
|
||||
| `Format` | string | Output format (json, junit, sarif, etc.) |
|
||||
| `Output` | string | Output file path |
|
||||
| `VerboseMode` | bool | Show all resources in output |
|
||||
| `FailThreshold` | float32 | Fail threshold percentage |
|
||||
| `ComplianceThreshold` | float32 | Compliance threshold percentage |
|
||||
| `UseExceptions` | string | Path to exceptions file |
|
||||
| `UseArtifactsFrom` | string | Path to offline artifacts |
|
||||
| `Submit` | bool | Submit results to SaaS |
|
||||
| `Local` | bool | Keep results local (don't submit) |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
results, err := ks.Scan(scanInfo)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
log.Fatal("Scan timed out")
|
||||
case errors.Is(err, context.Canceled):
|
||||
log.Fatal("Scan was canceled")
|
||||
default:
|
||||
log.Fatalf("Scan error: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The Kubescape instance is safe for concurrent use. You can run multiple scans in parallel:
|
||||
|
||||
```go
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, ns := range namespaces {
|
||||
wg.Add(1)
|
||||
go func(namespace string) {
|
||||
defer wg.Done()
|
||||
|
||||
scanInfo := &cautils.ScanInfo{
|
||||
IncludeNamespaces: namespace,
|
||||
}
|
||||
results, _ := ks.Scan(scanInfo)
|
||||
// Process results...
|
||||
}(ns)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [CLI Reference](../docs/cli-reference.md)
|
||||
- [Getting Started Guide](../docs/getting-started.md)
|
||||
- [Architecture](../docs/architecture.md)
|
||||
21
core/cautils/buildinfo.go
Normal file
21
core/cautils/buildinfo.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
)
|
||||
|
||||
var BuildNumber string
|
||||
var Client string
|
||||
|
||||
func init() {
|
||||
if BuildNumber != "" {
|
||||
versioncheck.BuildNumber = BuildNumber
|
||||
} else {
|
||||
versioncheck.BuildNumber = os.Getenv("RELEASE")
|
||||
}
|
||||
if Client != "" {
|
||||
versioncheck.Client = Client
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,24 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/google/uuid"
|
||||
v1 "github.com/kubescape/backend/pkg/client/v1"
|
||||
"github.com/kubescape/backend/pkg/servicediscovery"
|
||||
servicediscoveryv1 "github.com/kubescape/backend/pkg/servicediscovery/v1"
|
||||
servicediscoveryv2 "github.com/kubescape/backend/pkg/servicediscovery/v2"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
configFileName string = "config"
|
||||
kubescapeNamespace string = "kubescape"
|
||||
|
||||
kubescapeConfigMapName string = "kubescape-config" // deprecated - for backward compatibility
|
||||
kubescapeCloudConfigMapName string = "ks-cloud-config" // deprecated - for backward compatibility
|
||||
kubescapeConfigMapName string = "kubescape-config" // deprecated - for backward compatibility
|
||||
|
||||
cloudConfigMapLabelSelector string = "kubescape.io/infra=config"
|
||||
credsLabelSelectors string = "kubescape.io/infra=credentials" //nolint:gosec
|
||||
@@ -208,6 +206,8 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, accountID, accessKey, clu
|
||||
loadConfigFromFile(c.configObj)
|
||||
}
|
||||
|
||||
loadUrlsFromFile(c.configObj)
|
||||
|
||||
// second, load urls from config map
|
||||
c.updateConfigEmptyFieldsFromKubescapeConfigMap()
|
||||
|
||||
@@ -271,15 +271,12 @@ func (c *ClusterConfig) updateConfigEmptyFieldsFromKubescapeConfigMap() error {
|
||||
return err
|
||||
}
|
||||
var ksConfigMap *corev1.ConfigMap
|
||||
var urlsConfigMap *corev1.ConfigMap
|
||||
if len(configMaps.Items) == 0 {
|
||||
// try to find configmaps by name (for backward compatibility)
|
||||
ksConfigMap, _ = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), kubescapeConfigMapName, metav1.GetOptions{})
|
||||
urlsConfigMap, _ = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), kubescapeCloudConfigMapName, metav1.GetOptions{})
|
||||
} else {
|
||||
// use the first configmap with the label
|
||||
ksConfigMap = &configMaps.Items[0]
|
||||
urlsConfigMap = &configMaps.Items[0]
|
||||
}
|
||||
|
||||
if ksConfigMap != nil {
|
||||
@@ -292,30 +289,6 @@ func (c *ClusterConfig) updateConfigEmptyFieldsFromKubescapeConfigMap() error {
|
||||
}
|
||||
}
|
||||
|
||||
if urlsConfigMap != nil {
|
||||
if jsonConf, ok := urlsConfigMap.Data["services"]; ok {
|
||||
services, err := servicediscovery.GetServices(
|
||||
servicediscoveryv2.NewServiceDiscoveryStreamV2([]byte(jsonConf)),
|
||||
)
|
||||
if err != nil {
|
||||
// try to parse as v1
|
||||
services, err = servicediscovery.GetServices(
|
||||
servicediscoveryv1.NewServiceDiscoveryStreamV1([]byte(jsonConf)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if services.GetApiServerUrl() != "" {
|
||||
c.configObj.CloudAPIURL = services.GetApiServerUrl()
|
||||
}
|
||||
if services.GetReportReceiverHttpUrl() != "" {
|
||||
c.configObj.CloudReportURL = services.GetReportReceiverHttpUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -398,7 +371,7 @@ func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
|
||||
func loadConfigFromFile(configObj *ConfigObj) error {
|
||||
dat, err := os.ReadFile(ConfigFileFullPath())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil // no config file
|
||||
}
|
||||
return readConfig(dat, configObj)
|
||||
}
|
||||
@@ -414,6 +387,32 @@ func readConfig(dat []byte, configObj *ConfigObj) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadUrlsFromFile(obj *ConfigObj) error {
|
||||
dat, err := os.ReadFile("/etc/config/services.json")
|
||||
if err != nil {
|
||||
return nil // no config file
|
||||
}
|
||||
services, err := servicediscovery.GetServices(
|
||||
servicediscoveryv2.NewServiceDiscoveryStreamV2(dat),
|
||||
)
|
||||
if err != nil {
|
||||
// try to parse as v1
|
||||
services, err = servicediscovery.GetServices(
|
||||
servicediscoveryv1.NewServiceDiscoveryStreamV1(dat),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if services.GetApiServerUrl() != "" {
|
||||
obj.CloudAPIURL = services.GetApiServerUrl()
|
||||
}
|
||||
if services.GetReportReceiverHttpUrl() != "" {
|
||||
obj.CloudReportURL = services.GetReportReceiverHttpUrl()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteConfigFile() error {
|
||||
return os.Remove(ConfigFileFullPath())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
@@ -20,8 +23,14 @@ type K8SResources map[string][]string
|
||||
type ExternalResources map[string][]string
|
||||
|
||||
type ImageScanData struct {
|
||||
PresenterConfig *models.PresenterConfig
|
||||
Image string
|
||||
Context pkg.Context
|
||||
IgnoredMatches []match.IgnoredMatch
|
||||
Image string
|
||||
Matches match.Matches
|
||||
Packages []pkg.Package
|
||||
RemainingMatches *match.Matches
|
||||
SBOM *sbom.SBOM
|
||||
VulnerabilityProvider vulnerability.Provider
|
||||
}
|
||||
|
||||
type ScanTypes string
|
||||
@@ -59,6 +68,8 @@ type OPASessionObj struct {
|
||||
SingleResourceScan workloadinterface.IWorkload // single resource scan
|
||||
TopWorkloadsByScore []reporthandling.IResource
|
||||
TemplateMapping map[string]MappingNodes // Map chart obj to template (only for rendering from path)
|
||||
TriggeredByCLI bool
|
||||
LabelsToCopy []string // Labels to copy from workloads to scan reports
|
||||
}
|
||||
|
||||
func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework, k8sResources K8SResources, scanInfo *ScanInfo) *OPASessionObj {
|
||||
@@ -75,7 +86,9 @@ func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework
|
||||
SessionID: scanInfo.ScanID,
|
||||
Metadata: scanInfoToScanMetadata(ctx, scanInfo),
|
||||
OmitRawResources: scanInfo.OmitRawResources,
|
||||
TriggeredByCLI: scanInfo.TriggeredByCLI,
|
||||
TemplateMapping: make(map[string]MappingNodes),
|
||||
LabelsToCopy: scanInfo.LabelsToCopy,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func NewPolicies() *Policies {
|
||||
@@ -63,7 +63,7 @@ func (policies *Policies) Set(frameworks []reporthandling.Framework, excludedRul
|
||||
// 1. Rule is compatible with the current kubescape version
|
||||
// 2. Rule fits the current scanning scope
|
||||
func ShouldSkipRule(control reporthandling.Control, rule reporthandling.PolicyRule, scanningScope reporthandling.ScanningScopeType) bool {
|
||||
if !isRuleKubescapeVersionCompatible(rule.Attributes, BuildNumber) {
|
||||
if !isRuleKubescapeVersionCompatible(rule.Attributes, versioncheck.BuildNumber) {
|
||||
return true
|
||||
}
|
||||
if !isControlFitToScanScope(control, scanningScope) {
|
||||
|
||||
@@ -239,3 +239,59 @@ func TestIsFrameworkFitToScanScope(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var rule_v1_0_131 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useUntilKubescapeVersion": "v1.0.132"}}}
|
||||
var rule_v1_0_132 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.132", "useUntilKubescapeVersion": "v1.0.133"}}}
|
||||
var rule_v1_0_133 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.133", "useUntilKubescapeVersion": "v1.0.134"}}}
|
||||
var rule_v1_0_134 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.134"}}}
|
||||
var rule_invalid_from = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": 1.0135, "useUntilKubescapeVersion": "v1.0.135"}}}
|
||||
var rule_invalid_until = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.135", "useUntilKubescapeVersion": 1.0135}}}
|
||||
|
||||
func TestIsRuleKubescapeVersionCompatible(t *testing.T) {
|
||||
// local build- no build number
|
||||
|
||||
// should not crash when the value of useUntilKubescapeVersion is not a string
|
||||
buildNumberMock := "v1.0.135"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_invalid_from.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_invalid_until.Attributes, buildNumberMock))
|
||||
// should use only rules that don't have "until"
|
||||
buildNumberMock = ""
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.130"
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.132"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.133"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.135"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
spinnerpkg "github.com/briandowns/spinner"
|
||||
"github.com/jwalton/gchalk"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -38,7 +36,7 @@ type Chart struct {
|
||||
}
|
||||
|
||||
// LoadResourcesFromHelmCharts scans a given path (recursively) for helm charts, renders the templates and returns a map of workloads and a map of chart names
|
||||
func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[string][]workloadinterface.IMetadata, map[string]Chart, map[string]MappingNodes) {
|
||||
func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[string][]workloadinterface.IMetadata, map[string]Chart) {
|
||||
directories, _ := listDirs(basePath)
|
||||
helmDirectories := make([]string, 0)
|
||||
for _, dir := range directories {
|
||||
@@ -49,17 +47,14 @@ func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[stri
|
||||
|
||||
sourceToWorkloads := map[string][]workloadinterface.IMetadata{}
|
||||
sourceToChart := make(map[string]Chart, 0)
|
||||
sourceToNodes := map[string]MappingNodes{}
|
||||
for _, helmDir := range helmDirectories {
|
||||
chart, err := NewHelmChart(helmDir)
|
||||
if err == nil {
|
||||
wls, templateToNodes, errs := chart.GetWorkloadsWithDefaultValues()
|
||||
wls, errs := chart.GetWorkloadsWithDefaultValues()
|
||||
if len(errs) > 0 {
|
||||
logger.L().Ctx(ctx).Warning(fmt.Sprintf("Rendering of Helm chart template '%s', failed: %v", chart.GetName(), errs))
|
||||
continue
|
||||
}
|
||||
sourceToNodes = templateToNodes
|
||||
|
||||
chartName := chart.GetName()
|
||||
for k, v := range wls {
|
||||
sourceToWorkloads[k] = v
|
||||
@@ -68,12 +63,9 @@ func LoadResourcesFromHelmCharts(ctx context.Context, basePath string) (map[stri
|
||||
Path: helmDir,
|
||||
}
|
||||
}
|
||||
// for k, v := range templateMappings {
|
||||
// sourceToNodes[k] = v
|
||||
// }
|
||||
}
|
||||
}
|
||||
return sourceToWorkloads, sourceToChart, sourceToNodes
|
||||
return sourceToWorkloads, sourceToChart
|
||||
}
|
||||
|
||||
// If the contents at given path is a Kustomize Directory, LoadResourcesFromKustomizeDirectory will
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestLoadResourcesFromFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadResourcesFromHelmCharts(t *testing.T) {
|
||||
sourceToWorkloads, sourceToChartName, _ := LoadResourcesFromHelmCharts(context.TODO(), helmChartPath())
|
||||
sourceToWorkloads, sourceToChartName := LoadResourcesFromHelmCharts(context.TODO(), helmChartPath())
|
||||
assert.Equal(t, 6, len(sourceToWorkloads))
|
||||
|
||||
for file, workloads := range sourceToWorkloads {
|
||||
|
||||
@@ -5,11 +5,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/kubescape/opa-utils/reporthandling/attacktrack/v1alpha1"
|
||||
|
||||
"github.com/kubescape/regolibrary/gitregostore"
|
||||
"github.com/kubescape/regolibrary/v2/gitregostore"
|
||||
)
|
||||
|
||||
// =======================================================================================================================
|
||||
@@ -29,7 +27,7 @@ type DownloadReleasedPolicy struct {
|
||||
|
||||
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
|
||||
return &DownloadReleasedPolicy{
|
||||
gs: gitregostore.NewDefaultGitRegoStore(-1),
|
||||
gs: gitregostore.NewGitRegoStoreV2(-1),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,25 @@ package cautils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
|
||||
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
helmloader "helm.sh/helm/v3/pkg/chart/loader"
|
||||
helmchartutil "helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
helmdownloader "helm.sh/helm/v3/pkg/downloader"
|
||||
helmengine "helm.sh/helm/v3/pkg/engine"
|
||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||
helmregistry "helm.sh/helm/v3/pkg/registry"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
type HelmChart struct {
|
||||
@@ -26,7 +32,51 @@ func IsHelmDirectory(path string) (bool, error) {
|
||||
return helmchartutil.IsChartDir(path)
|
||||
}
|
||||
|
||||
// newRegistryClient creates a Helm registry client for chart authentication
|
||||
func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLS, plainHTTP bool, username, password string) (*helmregistry.Client, error) {
|
||||
// Basic client options with debug disabled
|
||||
opts := []helmregistry.ClientOption{
|
||||
helmregistry.ClientOptDebug(false),
|
||||
helmregistry.ClientOptWriter(io.Discard),
|
||||
}
|
||||
|
||||
// Add TLS certificates if provided
|
||||
if certFile != "" && keyFile != "" {
|
||||
opts = append(opts, helmregistry.ClientOptCredentialsFile(certFile))
|
||||
}
|
||||
|
||||
// Add CA certificate if provided
|
||||
if caFile != "" {
|
||||
opts = append(opts, helmregistry.ClientOptCredentialsFile(caFile))
|
||||
}
|
||||
|
||||
// Enable plain HTTP if needed
|
||||
if insecureSkipTLS {
|
||||
opts = append(opts, helmregistry.ClientOptPlainHTTP())
|
||||
}
|
||||
|
||||
registryClient, err := helmregistry.NewClient(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return registryClient, nil
|
||||
}
|
||||
|
||||
// defaultKeyring returns the default GPG keyring path for chart verification
|
||||
func defaultKeyring() string {
|
||||
if v, ok := os.LookupEnv("GNUPGHOME"); ok {
|
||||
return filepath.Join(v, "pubring.gpg")
|
||||
}
|
||||
return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
|
||||
}
|
||||
|
||||
func NewHelmChart(path string) (*HelmChart, error) {
|
||||
// Build chart dependencies before loading if Chart.lock exists
|
||||
if err := buildDependencies(path); err != nil {
|
||||
logger.L().Warning("Failed to build chart dependencies", helpers.String("path", path), helpers.Error(err))
|
||||
}
|
||||
|
||||
chart, err := helmloader.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -38,6 +88,35 @@ func NewHelmChart(path string) (*HelmChart, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildDependencies builds chart dependencies using the downloader manager
|
||||
func buildDependencies(chartPath string) error {
|
||||
// Create registry client for authentication
|
||||
registryClient, err := newRegistryClient("", "", "", false, false, "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create registry client: %w", err)
|
||||
}
|
||||
|
||||
// Create downloader manager with required configuration
|
||||
settings := cli.New()
|
||||
manager := &helmdownloader.Manager{
|
||||
Out: io.Discard, // Suppress output during scanning
|
||||
ChartPath: chartPath,
|
||||
Keyring: defaultKeyring(),
|
||||
SkipUpdate: false, // Allow updates to get latest dependencies
|
||||
Getters: helmgetter.All(settings),
|
||||
RegistryClient: registryClient,
|
||||
Debug: false,
|
||||
}
|
||||
|
||||
// Build dependencies from Chart.lock file
|
||||
err = manager.Build()
|
||||
if e, ok := err.(helmdownloader.ErrRepoNotFound); ok {
|
||||
return fmt.Errorf("%s. Please add missing repos via 'helm repo add'", e.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (hc *HelmChart) GetName() string {
|
||||
return hc.chart.Name()
|
||||
}
|
||||
@@ -46,38 +125,24 @@ func (hc *HelmChart) GetDefaultValues() map[string]interface{} {
|
||||
return hc.chart.Values
|
||||
}
|
||||
|
||||
// GetWorkloads renders chart template using the default values and returns a map of source file to its workloads
|
||||
func (hc *HelmChart) GetWorkloadsWithDefaultValues() (map[string][]workloadinterface.IMetadata, map[string]MappingNodes, []error) {
|
||||
// GetWorkloadsWithDefaultValues renders chart template using the default values and returns a map of source file to its workloads
|
||||
func (hc *HelmChart) GetWorkloadsWithDefaultValues() (map[string][]workloadinterface.IMetadata, []error) {
|
||||
return hc.GetWorkloads(hc.GetDefaultValues())
|
||||
}
|
||||
|
||||
// GetWorkloads renders chart template using the provided values and returns a map of source (absolute) file path to its workloads
|
||||
func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]workloadinterface.IMetadata, map[string]MappingNodes, []error) {
|
||||
func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]workloadinterface.IMetadata, []error) {
|
||||
vals, err := helmchartutil.ToRenderValues(hc.chart, values, helmchartutil.ReleaseOptions{}, nil)
|
||||
if err != nil {
|
||||
return nil, nil, []error{err}
|
||||
return nil, []error{err}
|
||||
}
|
||||
|
||||
// change the chart to template with comment, only is template(.yaml added otherwise no)
|
||||
hc.AddCommentToTemplate()
|
||||
|
||||
sourceToFile, err := helmengine.Render(hc.chart, vals)
|
||||
if err != nil {
|
||||
return nil, nil, []error{err}
|
||||
return nil, []error{err}
|
||||
}
|
||||
|
||||
// get the resouse and analysis and store it to the struct
|
||||
fileMapping := make(map[string]MappingNodes)
|
||||
err = GetTemplateMapping(sourceToFile, fileMapping)
|
||||
if err != nil {
|
||||
return nil, nil, []error{err}
|
||||
}
|
||||
|
||||
// delete the comment from chart and from sourceToFile
|
||||
RemoveComment(sourceToFile)
|
||||
|
||||
workloads := make(map[string][]workloadinterface.IMetadata, 0)
|
||||
errs := []error{}
|
||||
workloads := make(map[string][]workloadinterface.IMetadata)
|
||||
var errs []error
|
||||
|
||||
for path, renderedYaml := range sourceToFile {
|
||||
if !IsYaml(strings.ToLower(path)) {
|
||||
@@ -91,14 +156,9 @@ func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]w
|
||||
if len(wls) == 0 {
|
||||
continue
|
||||
}
|
||||
if firstPathSeparatorIndex := strings.Index(path, string("/")); firstPathSeparatorIndex != -1 {
|
||||
if firstPathSeparatorIndex := strings.Index(path, "/"); firstPathSeparatorIndex != -1 {
|
||||
absPath := filepath.Join(hc.path, path[firstPathSeparatorIndex:])
|
||||
|
||||
if nodes, ok := fileMapping[path]; ok {
|
||||
fileMapping[absPath] = nodes
|
||||
delete(fileMapping, path)
|
||||
}
|
||||
|
||||
workloads[absPath] = []workloadinterface.IMetadata{}
|
||||
for i := range wls {
|
||||
lw := localworkload.NewLocalWorkload(wls[i].GetObject())
|
||||
@@ -107,7 +167,7 @@ func (hc *HelmChart) GetWorkloads(values map[string]interface{}) (map[string][]w
|
||||
}
|
||||
}
|
||||
}
|
||||
return workloads, fileMapping, errs
|
||||
return workloads, errs
|
||||
}
|
||||
|
||||
func (hc *HelmChart) AddCommentToTemplate() {
|
||||
@@ -126,27 +186,3 @@ func (hc *HelmChart) AddCommentToTemplate() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveComment(sourceToFile map[string]string) {
|
||||
// commentRe := regexp.MustCompile(CommentFormat)
|
||||
for fileName, file := range sourceToFile {
|
||||
if !IsYaml(strings.ToLower((fileName))) {
|
||||
continue
|
||||
}
|
||||
sourceToFile[fileName] = commentRe.ReplaceAllLiteralString(file, "")
|
||||
}
|
||||
}
|
||||
|
||||
func GetTemplateMapping(sourceToFile map[string]string, fileMapping map[string]MappingNodes) error {
|
||||
for fileName, fileContent := range sourceToFile {
|
||||
mappingNodes, err := GetMapping(fileName, fileContent)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("GetMapping wrong, err: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
if len(mappingNodes.Nodes) != 0 {
|
||||
fileMapping[fileName] = *mappingNodes
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *HelmChartTestSuite) TestGetWorkloadsWithOverride() {
|
||||
// Override default value
|
||||
values["image"].(map[string]interface{})["pullPolicy"] = "Never"
|
||||
|
||||
fileToWorkloads, _, errs := chart.GetWorkloads(values)
|
||||
fileToWorkloads, errs := chart.GetWorkloads(values)
|
||||
s.Len(errs, 0)
|
||||
|
||||
s.Lenf(fileToWorkloads, len(s.expectedFiles), "Expected %d files", len(s.expectedFiles))
|
||||
@@ -111,7 +111,7 @@ func (s *HelmChartTestSuite) TestGetWorkloadsMissingValue() {
|
||||
values := chart.GetDefaultValues()
|
||||
delete(values, "image")
|
||||
|
||||
fileToWorkloads, _, errs := chart.GetWorkloads(values)
|
||||
fileToWorkloads, errs := chart.GetWorkloads(values)
|
||||
s.Nil(fileToWorkloads)
|
||||
s.Len(errs, 1, "Expected an error due to missing value")
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/krusty"
|
||||
"sigs.k8s.io/kustomize/api/types"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
@@ -76,7 +76,11 @@ func getKustomizeDirectoryName(path string) string {
|
||||
func (kd *KustomizeDirectory) GetWorkloads(kustomizeDirectoryPath string) (map[string][]workloadinterface.IMetadata, []error) {
|
||||
|
||||
fSys := filesys.MakeFsOnDisk()
|
||||
kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
|
||||
// Use LoadRestrictionsNone to allow loading resources from outside the kustomize directory.
|
||||
// This is necessary for overlays that reference base configurations in parent directories.
|
||||
opts := krusty.MakeDefaultOptions()
|
||||
opts.LoadRestrictions = types.LoadRestrictionsNone
|
||||
kustomizer := krusty.MakeKustomizer(opts)
|
||||
resmap, err := kustomizer.Run(fSys, kustomizeDirectoryPath)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetKustomizeDirectoryName(t *testing.T) {
|
||||
@@ -61,3 +63,83 @@ func TestGetKustomizeDirectoryName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func kustomizeTestdataPath() string {
|
||||
o, _ := os.Getwd()
|
||||
return filepath.Join(o, "testdata", "kustomize")
|
||||
}
|
||||
|
||||
// TestKustomizeOverlayWithBase tests that kustomize overlays can properly load
|
||||
// resources from base directories. This is the main fix for issue #1617.
|
||||
func TestKustomizeOverlayWithBase(t *testing.T) {
|
||||
overlayPath := filepath.Join(kustomizeTestdataPath(), "overlays", "prod")
|
||||
|
||||
// Verify it's detected as a kustomize directory
|
||||
assert.True(t, isKustomizeDirectory(overlayPath), "overlay should be detected as kustomize directory")
|
||||
|
||||
// Create kustomize directory and get workloads
|
||||
kd := NewKustomizeDirectory(overlayPath)
|
||||
workloads, errs := kd.GetWorkloads(overlayPath)
|
||||
|
||||
// Should not have errors - this was failing before the fix because
|
||||
// overlays couldn't load resources from parent base directories
|
||||
assert.Empty(t, errs, "should not have errors loading overlay with base reference")
|
||||
|
||||
// Should have workloads from the rendered overlay
|
||||
assert.NotEmpty(t, workloads, "should have workloads from rendered kustomize overlay")
|
||||
|
||||
// The overlay should have produced exactly one deployment with the merged configuration
|
||||
var deploymentFound bool
|
||||
for _, wls := range workloads {
|
||||
for _, wl := range wls {
|
||||
if wl.GetKind() == "Deployment" && wl.GetName() == "test-app" {
|
||||
deploymentFound = true
|
||||
|
||||
// Verify the deployment has the resource limits from the base
|
||||
obj := wl.GetObject()
|
||||
spec, ok := obj["spec"].(map[string]interface{})
|
||||
assert.True(t, ok, "deployment should have spec")
|
||||
|
||||
template, ok := spec["template"].(map[string]interface{})
|
||||
assert.True(t, ok, "deployment should have template")
|
||||
|
||||
templateSpec, ok := template["spec"].(map[string]interface{})
|
||||
assert.True(t, ok, "template should have spec")
|
||||
|
||||
containers, ok := templateSpec["containers"].([]interface{})
|
||||
assert.True(t, ok, "template spec should have containers")
|
||||
assert.NotEmpty(t, containers, "should have at least one container")
|
||||
|
||||
container, ok := containers[0].(map[string]interface{})
|
||||
assert.True(t, ok, "container should be a map")
|
||||
|
||||
resources, ok := container["resources"].(map[string]interface{})
|
||||
assert.True(t, ok, "container should have resources (from base)")
|
||||
|
||||
limits, ok := resources["limits"].(map[string]interface{})
|
||||
assert.True(t, ok, "resources should have limits")
|
||||
assert.Equal(t, "500m", limits["cpu"], "cpu limit should be from base")
|
||||
assert.Equal(t, "256Mi", limits["memory"], "memory limit should be from base")
|
||||
|
||||
// Verify overlay modifications were applied
|
||||
replicas, ok := spec["replicas"].(int)
|
||||
assert.True(t, ok, "replicas should be an int")
|
||||
assert.Equal(t, 3, replicas, "replicas should be modified by overlay")
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, deploymentFound, "deployment should be found in rendered output")
|
||||
}
|
||||
|
||||
// TestKustomizeBaseDirectory tests that base directories work on their own
|
||||
func TestKustomizeBaseDirectory(t *testing.T) {
|
||||
basePath := filepath.Join(kustomizeTestdataPath(), "base")
|
||||
|
||||
assert.True(t, isKustomizeDirectory(basePath), "base should be detected as kustomize directory")
|
||||
|
||||
kd := NewKustomizeDirectory(basePath)
|
||||
workloads, errs := kd.GetWorkloads(basePath)
|
||||
|
||||
assert.Empty(t, errs, "should not have errors loading base directory")
|
||||
assert.NotEmpty(t, workloads, "should have workloads from base directory")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/distribution/reference"
|
||||
)
|
||||
|
||||
func NormalizeImageName(img string) (string, error) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/mikefarah/yq/v4/pkg/yqlib"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
@@ -78,7 +77,7 @@ func GetMapping(fileName string, fileContent string) (*MappingNodes, error) {
|
||||
expression := fmt.Sprintf(lineExpression, index)
|
||||
output, err := getYamlLineInfo(expression, fileContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getYamlLineInfo wrong, the err is %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := extractParameter(pathRe, output, "$path")
|
||||
@@ -184,7 +183,7 @@ func getInfoFromOne(output string, lastNumber int, isMapType bool) (value string
|
||||
func getYamlLineInfo(expression string, yamlFile string) (string, error) {
|
||||
out, err := exectuateYq(expression, yamlFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exectuateYq err: %s", err.Error())
|
||||
return "", fmt.Errorf("exectuate yqlib err: %s", err.Error())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -203,7 +202,7 @@ func exectuateYq(expression string, yamlContent string) (string, error) {
|
||||
|
||||
out, err := stringEvaluator.Evaluate(expression, yamlContent, encoder, decoder)
|
||||
if err != nil {
|
||||
return "", errors.New("no matches found")
|
||||
return "", fmt.Errorf("no matches found")
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/kubescape/rbac-utils/rbacscanner"
|
||||
"github.com/kubescape/rbac-utils/rbacutils"
|
||||
)
|
||||
@@ -85,7 +84,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the correct apiVersion?
|
||||
crIMeta := workloadinterface.NewWorkloadObj(crmap)
|
||||
crIMeta.SetKind("ClusterRole")
|
||||
allresources[crIMeta.GetID()] = crIMeta
|
||||
@@ -95,7 +94,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the correct apiVersion?
|
||||
crIMeta := workloadinterface.NewWorkloadObj(crmap)
|
||||
crIMeta.SetKind("Role")
|
||||
allresources[crIMeta.GetID()] = crIMeta
|
||||
@@ -105,7 +104,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the correct apiVersion?
|
||||
crIMeta := workloadinterface.NewWorkloadObj(crmap)
|
||||
crIMeta.SetKind("ClusterRoleBinding")
|
||||
allresources[crIMeta.GetID()] = crIMeta
|
||||
@@ -115,7 +114,7 @@ func (rbacObjects *RBACObjects) rbacObjectsToResources(resources *rbacutils.Rbac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the the correct apiVersion?
|
||||
crmap["apiVersion"] = "rbac.authorization.k8s.io/v1" // TODO - is the correct apiVersion?
|
||||
crIMeta := workloadinterface.NewWorkloadObj(crmap)
|
||||
crIMeta.SetKind("RoleBinding")
|
||||
allresources[crIMeta.GetID()] = crIMeta
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package resourcehandler
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
nethttp "net/http"
|
||||
@@ -12,8 +13,44 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
giturl "github.com/kubescape/go-git-url"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
)
|
||||
|
||||
var tmpDirPaths map[string]string
|
||||
|
||||
func hashRepoURL(repoURL string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(repoURL))
|
||||
return string(h.Sum(nil))
|
||||
}
|
||||
|
||||
func getDirPath(repoURL string) string {
|
||||
if tmpDirPaths == nil {
|
||||
return ""
|
||||
}
|
||||
return tmpDirPaths[hashRepoURL(repoURL)]
|
||||
}
|
||||
|
||||
// Create a temporary directory this function is called once
|
||||
func createTempDir(repoURL string) (string, error) {
|
||||
tmpDirPath := getDirPath(repoURL)
|
||||
if tmpDirPath != "" {
|
||||
return tmpDirPath, nil
|
||||
}
|
||||
// create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temporary directory: %w", err)
|
||||
}
|
||||
if tmpDirPaths == nil {
|
||||
tmpDirPaths = make(map[string]string)
|
||||
}
|
||||
tmpDirPaths[hashRepoURL(repoURL)] = tmpDir
|
||||
|
||||
return tmpDir, nil
|
||||
}
|
||||
|
||||
// To Check if the given repository is Public(No Authentication needed), send a HTTP GET request to the URL
|
||||
// If response code is 200, the repository is Public.
|
||||
func isGitRepoPublic(u string) bool {
|
||||
@@ -55,34 +92,38 @@ func getProviderError(gitURL giturl.IGitAPI) error {
|
||||
|
||||
// cloneRepo clones a repository to a local temporary directory and returns the directory
|
||||
func cloneRepo(gitURL giturl.IGitAPI) (string, error) {
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temporary directory: %w", err)
|
||||
}
|
||||
|
||||
// Get the URL to clone
|
||||
cloneURL := gitURL.GetHttpCloneURL()
|
||||
|
||||
isGitRepoPublic := isGitRepoPublic(cloneURL)
|
||||
// Check if directory exists
|
||||
if p := getDirPath(cloneURL); p != "" {
|
||||
// directory exists, meaning this repo was cloned
|
||||
return p, nil
|
||||
}
|
||||
// Get the URL to clone
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := createTempDir(cloneURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
isGitTokenPresent := isGitTokenPresent(gitURL)
|
||||
|
||||
// Declare the authentication variable required for cloneOptions
|
||||
var auth transport.AuthMethod
|
||||
|
||||
if isGitRepoPublic {
|
||||
// No authentication needed if repository is public
|
||||
auth = nil
|
||||
} else {
|
||||
|
||||
// Return Error if the AUTH_TOKEN is not present
|
||||
if isGitTokenPresent := isGitTokenPresent(gitURL); !isGitTokenPresent {
|
||||
return "", getProviderError(gitURL)
|
||||
}
|
||||
if isGitTokenPresent {
|
||||
auth = &http.BasicAuth{
|
||||
Username: "x-token-auth",
|
||||
Password: gitURL.GetToken(),
|
||||
}
|
||||
} else {
|
||||
// If the repository is public, no authentication is needed
|
||||
if isGitRepoPublic(cloneURL) {
|
||||
auth = nil
|
||||
} else {
|
||||
return "", getProviderError(gitURL)
|
||||
}
|
||||
}
|
||||
|
||||
// For Azure repo cloning
|
||||
@@ -102,6 +143,42 @@ func cloneRepo(gitURL giturl.IGitAPI) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to clone %s. %w", gitURL.GetRepoName(), err)
|
||||
}
|
||||
// tmpDir = filepath.Join(tmpDir, gitURL.GetRepoName())
|
||||
tmpDirPaths[hashRepoURL(cloneURL)] = tmpDir
|
||||
|
||||
return tmpDir, nil
|
||||
}
|
||||
|
||||
// CloneGitRepo clone git repository
|
||||
func CloneGitRepo(path *string) (string, error) {
|
||||
var clonedDir string
|
||||
|
||||
gitURL, err := giturl.NewGitAPI(*path)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Clone git repository if needed
|
||||
logger.L().Start("cloning", helpers.String("repository url", gitURL.GetURL().String()))
|
||||
|
||||
clonedDir, err = cloneRepo(gitURL)
|
||||
if err != nil {
|
||||
logger.L().StopError("failed to clone git repo", helpers.String("url", gitURL.GetURL().String()), helpers.Error(err))
|
||||
return "", fmt.Errorf("failed to clone git repo '%s', %w", gitURL.GetURL().String(), err)
|
||||
}
|
||||
*path = clonedDir
|
||||
|
||||
logger.L().StopSuccess("Done accessing remote repo")
|
||||
|
||||
return clonedDir, nil
|
||||
}
|
||||
|
||||
func GetClonedPath(path string) string {
|
||||
|
||||
gitURL, err := giturl.NewGitAPI(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return getDirPath(gitURL.GetHttpCloneURL())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package resourcehandler
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -93,3 +93,63 @@ func TestCloneRepo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestGetClonedPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Valid Git URL",
|
||||
path: "https://github.com/kubescape/kubescape.git",
|
||||
expected: "/path/to/cloned/repo", // replace with the expected path
|
||||
},
|
||||
{
|
||||
name: "Invalid Git URL",
|
||||
path: "invalid",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
tmpDirPaths = make(map[string]string)
|
||||
tmpDirPaths[hashRepoURL("https://github.com/kubescape/kubescape.git")] = "/path/to/cloned/repo" // replace with the actual path
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := GetClonedPath(tc.path)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestGetDirPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
repoURL string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Existing Repo URL",
|
||||
repoURL: "https://github.com/user/repo.git",
|
||||
expected: "/path/to/cloned/repo", // replace with the expected path
|
||||
},
|
||||
{
|
||||
name: "Non-Existing Repo URL",
|
||||
repoURL: "https://github.com/user/nonexistentrepo.git",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize tmpDirPaths
|
||||
tmpDirPaths = make(map[string]string)
|
||||
tmpDirPaths[hashRepoURL("https://github.com/user/repo.git")] = "/path/to/cloned/repo" // replace with the actual path
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := getDirPath(tc.repoURL)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ type RootInfo struct {
|
||||
Logger string // logger level
|
||||
LoggerName string // logger name ("pretty"/"zap"/"none")
|
||||
CacheDir string // cached dir
|
||||
DisableColor bool // Disable Color
|
||||
EnableColor bool // Force enable Color
|
||||
DiscoveryServerURL string // Discovery Server URL (See https://github.com/kubescape/backend/tree/main/pkg/servicediscovery)
|
||||
KubeContext string // context name
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
giturl "github.com/kubescape/go-git-url"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
@@ -17,27 +19,22 @@ import (
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ScanningContext string
|
||||
|
||||
const (
|
||||
ContextCluster ScanningContext = "cluster"
|
||||
ContextFile ScanningContext = "single-file"
|
||||
ContextDir ScanningContext = "local-dir"
|
||||
ContextGitURL ScanningContext = "git-url"
|
||||
ContextGitLocal ScanningContext = "git-local"
|
||||
ContextCluster ScanningContext = "cluster"
|
||||
ContextFile ScanningContext = "single-file"
|
||||
ContextDir ScanningContext = "local-dir"
|
||||
ContextGitLocal ScanningContext = "git-local"
|
||||
ContextGitRemote ScanningContext = "git-remote"
|
||||
)
|
||||
|
||||
const ( // deprecated
|
||||
ScopeCluster = "cluster"
|
||||
ScopeYAML = "yaml"
|
||||
)
|
||||
const (
|
||||
// ScanCluster string = "cluster"
|
||||
// ScanLocalFiles string = "yaml"
|
||||
localControlInputsFilename string = "controls-inputs.json"
|
||||
LocalExceptionsFilename string = "exceptions.json"
|
||||
LocalAttackTracksFilename string = "attack-tracks.json"
|
||||
@@ -110,8 +107,8 @@ type ScanInfo struct {
|
||||
UseFrom []string // Load framework from local file (instead of download). Use when running offline
|
||||
UseDefault bool // Load framework from cached file (instead of download). Use when running offline
|
||||
UseArtifactsFrom string // Load artifacts from local path. Use when running offline
|
||||
VerboseMode bool // Display all of the input resources and not only failed resources
|
||||
View string // Display all of the input resources and not only failed resources
|
||||
VerboseMode bool // Display all the input resources and not only failed resources
|
||||
View string //
|
||||
Format string // Format results (table, json, junit ...)
|
||||
Output string // Store results in an output file, Output file name
|
||||
FormatVersion string // Output object can be different between versions, this is for testing and backward compatibility
|
||||
@@ -134,12 +131,18 @@ type ScanInfo struct {
|
||||
ScanAll bool // true if scan all frameworks
|
||||
OmitRawResources bool // true if omit raw resources from the output
|
||||
PrintAttackTree bool // true if print attack tree
|
||||
EnableRegoPrint bool // true if print rego
|
||||
ScanObject *objectsenvelopes.ScanObject // identifies a single resource (k8s object) to be scanned
|
||||
IsDeletedScanObject bool // indicates whether the ScanObject is a deleted K8S resource
|
||||
TriggeredByCLI bool // indicates whether the scan was triggered by the CLI
|
||||
ScanType ScanTypes
|
||||
ScanImages bool
|
||||
UseDefaultMatchers bool
|
||||
ChartPath string
|
||||
FilePath string
|
||||
LabelsToCopy []string // Labels to copy from workloads to scan reports
|
||||
scanningContext *ScanningContext
|
||||
cleanups []func()
|
||||
}
|
||||
|
||||
type Getters struct {
|
||||
@@ -155,7 +158,12 @@ func (scanInfo *ScanInfo) Init(ctx context.Context) {
|
||||
if scanInfo.ScanID == "" {
|
||||
scanInfo.ScanID = uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) Cleanup() {
|
||||
for _, cleanup := range scanInfo.cleanups {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setUseArtifactsFrom(ctx context.Context) {
|
||||
@@ -259,7 +267,7 @@ func scanInfoToScanMetadata(ctx context.Context, scanInfo *ScanInfo) *reporthand
|
||||
metadata.ScanMetadata.TargetNames = append(metadata.ScanMetadata.TargetNames, policy.Identifier)
|
||||
}
|
||||
|
||||
metadata.ScanMetadata.KubescapeVersion = BuildNumber
|
||||
metadata.ScanMetadata.KubescapeVersion = versioncheck.BuildNumber
|
||||
metadata.ScanMetadata.VerboseMode = scanInfo.VerboseMode
|
||||
metadata.ScanMetadata.FailThreshold = scanInfo.FailThreshold
|
||||
metadata.ScanMetadata.ComplianceThreshold = scanInfo.ComplianceThreshold
|
||||
@@ -267,51 +275,78 @@ func scanInfoToScanMetadata(ctx context.Context, scanInfo *ScanInfo) *reporthand
|
||||
metadata.ScanMetadata.VerboseMode = scanInfo.VerboseMode
|
||||
metadata.ScanMetadata.ControlsInputs = scanInfo.ControlsInputs
|
||||
|
||||
inputFiles := ""
|
||||
if len(scanInfo.InputPatterns) > 0 {
|
||||
inputFiles = scanInfo.InputPatterns[0]
|
||||
}
|
||||
switch GetScanningContext(inputFiles) {
|
||||
switch scanInfo.GetScanningContext() {
|
||||
case ContextCluster:
|
||||
// cluster
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Cluster
|
||||
case ContextFile:
|
||||
// local file
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.File
|
||||
case ContextGitURL:
|
||||
// url
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Repo
|
||||
case ContextGitLocal:
|
||||
// local-git
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.GitLocal
|
||||
case ContextGitRemote:
|
||||
// remote
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Repo
|
||||
case ContextDir:
|
||||
// directory
|
||||
metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Directory
|
||||
|
||||
}
|
||||
|
||||
setContextMetadata(ctx, &metadata.ContextMetadata, inputFiles)
|
||||
scanInfo.setContextMetadata(ctx, &metadata.ContextMetadata)
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) GetScanningContext() ScanningContext {
|
||||
func (scanInfo *ScanInfo) GetInputFiles() string {
|
||||
if len(scanInfo.InputPatterns) > 0 {
|
||||
return GetScanningContext(scanInfo.InputPatterns[0])
|
||||
return scanInfo.InputPatterns[0]
|
||||
}
|
||||
return GetScanningContext("")
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetScanningContext get scanning context from the input param
|
||||
func GetScanningContext(input string) ScanningContext {
|
||||
func (scanInfo *ScanInfo) GetScanningContext() ScanningContext {
|
||||
if scanInfo.scanningContext == nil {
|
||||
scanningContext := scanInfo.getScanningContext(scanInfo.GetInputFiles())
|
||||
scanInfo.scanningContext = &scanningContext
|
||||
}
|
||||
return *scanInfo.scanningContext
|
||||
}
|
||||
|
||||
// getScanningContext get scanning context from the input param
|
||||
// this function should be called only once. Call GetScanningContext() to get the scanning context
|
||||
func (scanInfo *ScanInfo) getScanningContext(input string) ScanningContext {
|
||||
// cluster
|
||||
if input == "" {
|
||||
return ContextCluster
|
||||
}
|
||||
|
||||
// url
|
||||
// Check if input is a URL (http:// or https://)
|
||||
isURL := isHTTPURL(input)
|
||||
|
||||
// git url
|
||||
if _, err := giturl.NewGitURL(input); err == nil {
|
||||
return ContextGitURL
|
||||
if repo, err := CloneGitRepo(&input); err == nil {
|
||||
if _, err := NewLocalGitRepository(repo); err == nil {
|
||||
scanInfo.cleanups = append(scanInfo.cleanups, func() {
|
||||
_ = os.RemoveAll(repo)
|
||||
})
|
||||
return ContextGitRemote
|
||||
}
|
||||
}
|
||||
// If giturl.NewGitURL succeeded but cloning failed, the input is a git URL
|
||||
// that couldn't be cloned. Don't treat it as a local path.
|
||||
// The clone error was already logged by CloneGitRepo.
|
||||
// Return ContextDir to prevent the URL from being joined with the current directory
|
||||
// and to trigger a "no files found" error with the actual URL (not a mangled path).
|
||||
return ContextDir
|
||||
}
|
||||
|
||||
// If it looks like a URL but wasn't recognized as a git URL, still don't treat it as a local path
|
||||
if isURL {
|
||||
logger.L().Error("URL provided but not recognized as a valid git repository. Ensure the URL is correct and accessible", helpers.String("url", input))
|
||||
return ContextDir
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(input) { // parse path
|
||||
@@ -333,19 +368,14 @@ func GetScanningContext(input string) ScanningContext {
|
||||
// dir/glob
|
||||
return ContextDir
|
||||
}
|
||||
func setContextMetadata(ctx context.Context, contextMetadata *reporthandlingv2.ContextMetadata, input string) {
|
||||
switch GetScanningContext(input) {
|
||||
|
||||
func (scanInfo *ScanInfo) setContextMetadata(ctx context.Context, contextMetadata *reporthandlingv2.ContextMetadata) {
|
||||
input := scanInfo.GetInputFiles()
|
||||
switch scanInfo.GetScanningContext() {
|
||||
case ContextCluster:
|
||||
contextMetadata.ClusterContextMetadata = &reporthandlingv2.ClusterMetadata{
|
||||
ContextName: k8sinterface.GetContextName(),
|
||||
}
|
||||
case ContextGitURL:
|
||||
// url
|
||||
context, err := metadataGitURL(input)
|
||||
if err != nil {
|
||||
logger.L().Ctx(ctx).Warning("in setContextMetadata", helpers.Interface("case", ContextGitURL), helpers.Error(err))
|
||||
}
|
||||
contextMetadata.RepoContextMetadata = context
|
||||
case ContextDir:
|
||||
contextMetadata.DirectoryContextMetadata = &reporthandlingv2.DirectoryContextMetadata{
|
||||
BasePath: getAbsPath(input),
|
||||
@@ -377,43 +407,21 @@ func setContextMetadata(ctx context.Context, contextMetadata *reporthandlingv2.C
|
||||
}
|
||||
case ContextGitLocal:
|
||||
// local
|
||||
context, err := metadataGitLocal(input)
|
||||
repoContext, err := metadataGitLocal(input)
|
||||
if err != nil {
|
||||
logger.L().Ctx(ctx).Warning("in setContextMetadata", helpers.Interface("case", ContextGitURL), helpers.Error(err))
|
||||
logger.L().Ctx(ctx).Warning("in setContextMetadata", helpers.Interface("case", ContextGitLocal), helpers.Error(err))
|
||||
}
|
||||
contextMetadata.RepoContextMetadata = context
|
||||
contextMetadata.RepoContextMetadata = repoContext
|
||||
case ContextGitRemote:
|
||||
// remote
|
||||
repoContext, err := metadataGitLocal(GetClonedPath(input))
|
||||
if err != nil {
|
||||
logger.L().Ctx(ctx).Warning("in setContextMetadata", helpers.Interface("case", ContextGitRemote), helpers.Error(err))
|
||||
}
|
||||
contextMetadata.RepoContextMetadata = repoContext
|
||||
}
|
||||
}
|
||||
|
||||
func metadataGitURL(input string) (*reporthandlingv2.RepoContextMetadata, error) {
|
||||
context := &reporthandlingv2.RepoContextMetadata{}
|
||||
gitParser, err := giturl.NewGitAPI(input)
|
||||
if err != nil {
|
||||
return context, fmt.Errorf("%w", err)
|
||||
}
|
||||
if gitParser.GetBranchName() == "" {
|
||||
gitParser.SetDefaultBranchName()
|
||||
}
|
||||
context.Provider = gitParser.GetProvider()
|
||||
context.Repo = gitParser.GetRepoName()
|
||||
context.Owner = gitParser.GetOwnerName()
|
||||
context.Branch = gitParser.GetBranchName()
|
||||
context.RemoteURL = gitParser.GetURL().String()
|
||||
|
||||
commit, err := gitParser.GetLatestCommit()
|
||||
if err != nil {
|
||||
return context, fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
context.LastCommit = reporthandling.LastCommit{
|
||||
Hash: commit.SHA,
|
||||
Date: commit.Committer.Date,
|
||||
CommitterName: commit.Committer.Name,
|
||||
}
|
||||
|
||||
return context, nil
|
||||
}
|
||||
|
||||
func metadataGitLocal(input string) (*reporthandlingv2.RepoContextMetadata, error) {
|
||||
gitParser, err := NewLocalGitRepository(input)
|
||||
if err != nil {
|
||||
@@ -423,31 +431,31 @@ func metadataGitLocal(input string) (*reporthandlingv2.RepoContextMetadata, erro
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w", err)
|
||||
}
|
||||
context := &reporthandlingv2.RepoContextMetadata{}
|
||||
repoContext := &reporthandlingv2.RepoContextMetadata{}
|
||||
gitParserURL, err := giturl.NewGitURL(remoteURL)
|
||||
if err != nil {
|
||||
return context, fmt.Errorf("%w", err)
|
||||
return repoContext, fmt.Errorf("%w", err)
|
||||
}
|
||||
gitParserURL.SetBranchName(gitParser.GetBranchName())
|
||||
|
||||
context.Provider = gitParserURL.GetProvider()
|
||||
context.Repo = gitParserURL.GetRepoName()
|
||||
context.Owner = gitParserURL.GetOwnerName()
|
||||
context.Branch = gitParserURL.GetBranchName()
|
||||
context.RemoteURL = gitParserURL.GetURL().String()
|
||||
repoContext.Provider = gitParserURL.GetProvider()
|
||||
repoContext.Repo = gitParserURL.GetRepoName()
|
||||
repoContext.Owner = gitParserURL.GetOwnerName()
|
||||
repoContext.Branch = gitParserURL.GetBranchName()
|
||||
repoContext.RemoteURL = gitParserURL.GetURL().String()
|
||||
|
||||
commit, err := gitParser.GetLastCommit()
|
||||
if err != nil {
|
||||
return context, fmt.Errorf("%w", err)
|
||||
return repoContext, fmt.Errorf("%w", err)
|
||||
}
|
||||
context.LastCommit = reporthandling.LastCommit{
|
||||
repoContext.LastCommit = reporthandling.LastCommit{
|
||||
Hash: commit.SHA,
|
||||
Date: commit.Committer.Date,
|
||||
CommitterName: commit.Committer.Name,
|
||||
}
|
||||
context.LocalRootPath, _ = gitParser.GetRootDir()
|
||||
repoContext.LocalRootPath, _ = gitParser.GetRootDir()
|
||||
|
||||
return context, nil
|
||||
return repoContext, nil
|
||||
}
|
||||
func getHostname() string {
|
||||
if h, e := os.Hostname(); e == nil {
|
||||
@@ -465,10 +473,7 @@ func getAbsPath(p string) string {
|
||||
return p
|
||||
}
|
||||
|
||||
// ScanningContextToScanningScope convert the context to the deprecated scope
|
||||
func ScanningContextToScanningScope(scanningContext ScanningContext) string {
|
||||
if scanningContext == ContextCluster {
|
||||
return ScopeCluster
|
||||
}
|
||||
return ScopeYAML
|
||||
// isHTTPURL checks if the input string is an HTTP or HTTPS URL
|
||||
func isHTTPURL(input string) bool {
|
||||
return strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://")
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ package cautils
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetContextMetadata(t *testing.T) {
|
||||
{
|
||||
ctx := reporthandlingv2.ContextMetadata{}
|
||||
setContextMetadata(context.TODO(), &ctx, "")
|
||||
scanInfo := &ScanInfo{}
|
||||
scanInfo.setContextMetadata(context.TODO(), &ctx)
|
||||
|
||||
assert.NotNil(t, ctx.ClusterContextMetadata)
|
||||
assert.Nil(t, ctx.DirectoryContextMetadata)
|
||||
@@ -42,13 +44,67 @@ func TestGetHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetScanningContext(t *testing.T) {
|
||||
// Test with empty input
|
||||
assert.Equal(t, ContextCluster, GetScanningContext(""))
|
||||
|
||||
// Test with Git URL input
|
||||
assert.Equal(t, ContextGitURL, GetScanningContext("https://github.com/kubescape/kubescape"))
|
||||
|
||||
// TODO: Add more tests with other input types
|
||||
repoRoot, err := os.MkdirTemp("", "repo")
|
||||
require.NoError(t, err)
|
||||
defer func(name string) {
|
||||
_ = os.Remove(name)
|
||||
}(repoRoot)
|
||||
_, err = git.PlainClone(repoRoot, false, &git.CloneOptions{
|
||||
URL: "https://github.com/kubescape/http-request",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tmpFile, err := os.CreateTemp("", "single.*.txt")
|
||||
require.NoError(t, err)
|
||||
defer func(name string) {
|
||||
_ = os.Remove(name)
|
||||
}(tmpFile.Name())
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want ScanningContext
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
want: ContextCluster,
|
||||
},
|
||||
{
|
||||
name: "git URL input",
|
||||
input: "https://github.com/kubescape/http-request",
|
||||
want: ContextGitRemote,
|
||||
},
|
||||
{
|
||||
name: "local git input",
|
||||
input: repoRoot,
|
||||
want: ContextGitLocal,
|
||||
},
|
||||
{
|
||||
name: "single file input",
|
||||
input: tmpFile.Name(),
|
||||
want: ContextFile,
|
||||
},
|
||||
{
|
||||
name: "directory input",
|
||||
input: os.TempDir(),
|
||||
want: ContextDir,
|
||||
},
|
||||
{
|
||||
name: "self-hosted GitLab URL that can't be cloned",
|
||||
input: "https://gitlab.private-domain.com/my-org/my-repo.git",
|
||||
want: ContextDir, // Should return ContextDir when clone fails, not try to treat as local path
|
||||
},
|
||||
{
|
||||
name: "http URL that can't be cloned",
|
||||
input: "http://gitlab.example.com/org/repo",
|
||||
want: ContextDir, // Should return ContextDir when clone fails, not try to treat as local path
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanInfo := &ScanInfo{}
|
||||
assert.Equalf(t, tt.want, scanInfo.getScanningContext(tt.input), "GetScanningContext(%v)", tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanInfoFormats(t *testing.T) {
|
||||
@@ -77,30 +133,3 @@ func TestScanInfoFormats(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetScanningContextWithFile(t *testing.T) {
|
||||
// Test with a file
|
||||
dir, err := os.MkdirTemp("", "example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
filePath := filepath.Join(dir, "file.txt")
|
||||
if _, err := os.Create(filePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, ContextFile, GetScanningContext(filePath))
|
||||
}
|
||||
|
||||
func TestGetScanningContextWithDir(t *testing.T) {
|
||||
// Test with a directory
|
||||
dir, err := os.MkdirTemp("", "example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
assert.Equal(t, ContextDir, GetScanningContext(dir))
|
||||
}
|
||||
|
||||
28
core/cautils/testdata/kustomize/base/deployment.yaml
vendored
Normal file
28
core/cautils/testdata/kustomize/base/deployment.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-app
|
||||
labels:
|
||||
app: test-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: test-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: test-app
|
||||
spec:
|
||||
containers:
|
||||
- name: test-container
|
||||
image: nginx:1.19
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
5
core/cautils/testdata/kustomize/base/kustomization.yaml
vendored
Normal file
5
core/cautils/testdata/kustomize/base/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
13
core/cautils/testdata/kustomize/overlays/prod/kustomization.yaml
vendored
Normal file
13
core/cautils/testdata/kustomize/overlays/prod/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
images:
|
||||
- name: nginx
|
||||
newTag: "1.21"
|
||||
|
||||
replicas:
|
||||
- name: test-app
|
||||
count: 3
|
||||
@@ -1,186 +0,0 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/utils-go/boolutils"
|
||||
utils "github.com/kubescape/backend/pkg/utils"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
"github.com/mattn/go-isatty"
|
||||
"go.opentelemetry.io/otel"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const SKIP_VERSION_CHECK_DEPRECATED_ENV = "KUBESCAPE_SKIP_UPDATE_CHECK"
|
||||
const SKIP_VERSION_CHECK_ENV = "KS_SKIP_UPDATE_CHECK"
|
||||
const CLIENT_ENV = "KS_CLIENT"
|
||||
|
||||
var BuildNumber string
|
||||
var Client string
|
||||
var LatestReleaseVersion string
|
||||
|
||||
const UnknownBuildNumber = "unknown"
|
||||
|
||||
type IVersionCheckHandler interface {
|
||||
CheckLatestVersion(context.Context, *VersionCheckRequest) error
|
||||
}
|
||||
|
||||
func NewIVersionCheckHandler(ctx context.Context) IVersionCheckHandler {
|
||||
if BuildNumber == "" {
|
||||
logger.L().Ctx(ctx).Warning("Unknown build number: this might affect your scan results. Please ensure that you are running the latest version.")
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv(CLIENT_ENV); ok && v != "" {
|
||||
Client = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv(SKIP_VERSION_CHECK_ENV); ok && boolutils.StringToBool(v) {
|
||||
return NewVersionCheckHandlerMock()
|
||||
} else if v, ok := os.LookupEnv(SKIP_VERSION_CHECK_DEPRECATED_ENV); ok && boolutils.StringToBool(v) {
|
||||
return NewVersionCheckHandlerMock()
|
||||
}
|
||||
return NewVersionCheckHandler()
|
||||
}
|
||||
|
||||
type VersionCheckHandlerMock struct {
|
||||
}
|
||||
|
||||
func NewVersionCheckHandlerMock() *VersionCheckHandlerMock {
|
||||
return &VersionCheckHandlerMock{}
|
||||
}
|
||||
|
||||
type VersionCheckHandler struct {
|
||||
versionURL string
|
||||
}
|
||||
type VersionCheckRequest struct {
|
||||
Client string `json:"client"` // kubescape
|
||||
ClientBuild string `json:"clientBuild"` // client build environment
|
||||
ClientVersion string `json:"clientVersion"` // kubescape version
|
||||
Framework string `json:"framework"` // framework name
|
||||
FrameworkVersion string `json:"frameworkVersion"` // framework version
|
||||
ScanningTarget string `json:"target"` // Deprecated
|
||||
ScanningContext string `json:"context"` // scanning context- cluster/file/gitURL/localGit/dir
|
||||
TriggeredBy string `json:"triggeredBy"` // triggered by - cli/ ci / microservice
|
||||
}
|
||||
|
||||
type VersionCheckResponse struct {
|
||||
Client string `json:"client"` // kubescape
|
||||
ClientUpdate string `json:"clientUpdate"` // kubescape latest version
|
||||
Framework string `json:"framework"` // framework name
|
||||
FrameworkUpdate string `json:"frameworkUpdate"` // framework latest version
|
||||
Message string `json:"message"` // alert message
|
||||
}
|
||||
|
||||
func NewVersionCheckHandler() *VersionCheckHandler {
|
||||
return &VersionCheckHandler{
|
||||
versionURL: "https://us-central1-elated-pottery-310110.cloudfunctions.net/ksgf1v1",
|
||||
}
|
||||
}
|
||||
|
||||
func getTriggerSource() string {
|
||||
if strings.Contains(os.Args[0], "ksserver") {
|
||||
return "microservice"
|
||||
}
|
||||
|
||||
if !isatty.IsTerminal(os.Stdin.Fd()) && !isatty.IsCygwinTerminal(os.Stdin.Fd()) {
|
||||
// non-interactive shell
|
||||
return "pipeline"
|
||||
}
|
||||
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
return "pipeline"
|
||||
}
|
||||
|
||||
return "cli"
|
||||
}
|
||||
|
||||
func NewVersionCheckRequest(buildNumber, frameworkName, frameworkVersion, scanningTarget string) *VersionCheckRequest {
|
||||
if buildNumber == "" {
|
||||
buildNumber = UnknownBuildNumber
|
||||
}
|
||||
|
||||
if scanningTarget == "" {
|
||||
scanningTarget = "unknown"
|
||||
}
|
||||
|
||||
if Client == "" {
|
||||
Client = "local-build"
|
||||
}
|
||||
|
||||
return &VersionCheckRequest{
|
||||
Client: "kubescape",
|
||||
ClientBuild: Client,
|
||||
ClientVersion: buildNumber,
|
||||
Framework: frameworkName,
|
||||
FrameworkVersion: frameworkVersion,
|
||||
ScanningTarget: scanningTarget,
|
||||
TriggeredBy: getTriggerSource(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VersionCheckHandlerMock) CheckLatestVersion(_ context.Context, _ *VersionCheckRequest) error {
|
||||
logger.L().Info("Skipping version check")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VersionCheckHandler) CheckLatestVersion(ctx context.Context, versionData *VersionCheckRequest) error {
|
||||
ctx, span := otel.Tracer("").Start(ctx, "versionCheckHandler.CheckLatestVersion")
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.L().Ctx(ctx).Warning("failed to get latest version", helpers.Interface("error", err))
|
||||
}
|
||||
}()
|
||||
|
||||
latestVersion, err := v.getLatestVersion(versionData)
|
||||
if err != nil || latestVersion == nil {
|
||||
return fmt.Errorf("failed to get latest version")
|
||||
}
|
||||
|
||||
LatestReleaseVersion = latestVersion.ClientUpdate
|
||||
|
||||
if latestVersion.ClientUpdate != "" {
|
||||
if BuildNumber != "" && semver.Compare(BuildNumber, LatestReleaseVersion) == -1 {
|
||||
logger.L().Ctx(ctx).Warning(warningMessage(LatestReleaseVersion))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Enable after supporting framework version
|
||||
// if latestVersion.FrameworkUpdate != "" {
|
||||
// fmt.Println(warningMessage(latestVersion.Framework, latestVersion.FrameworkUpdate))
|
||||
// }
|
||||
|
||||
if latestVersion.Message != "" {
|
||||
logger.L().Info(latestVersion.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VersionCheckHandler) getLatestVersion(versionData *VersionCheckRequest) (*VersionCheckResponse, error) {
|
||||
|
||||
reqBody, err := json.Marshal(*versionData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in 'CheckLatestVersion' failed to json.Marshal, reason: %s", err.Error())
|
||||
}
|
||||
|
||||
rdr, _, err := getter.HTTPPost(http.DefaultClient, v.versionURL, reqBody, map[string]string{"Content-Type": "application/json"})
|
||||
|
||||
vResp, err := utils.Decode[*VersionCheckResponse](rdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vResp, nil
|
||||
}
|
||||
|
||||
func warningMessage(release string) string {
|
||||
return fmt.Sprintf("current version '%s' is not updated to the latest release: '%s'", BuildNumber, release)
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func TestGetKubernetesObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
var rule_v1_0_131 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useUntilKubescapeVersion": "v1.0.132"}}}
|
||||
var rule_v1_0_132 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.132", "useUntilKubescapeVersion": "v1.0.133"}}}
|
||||
var rule_v1_0_133 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.133", "useUntilKubescapeVersion": "v1.0.134"}}}
|
||||
var rule_v1_0_134 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.134"}}}
|
||||
var rule_invalid_from = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": 1.0135, "useUntilKubescapeVersion": "v1.0.135"}}}
|
||||
var rule_invalid_until = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{
|
||||
Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.135", "useUntilKubescapeVersion": 1.0135}}}
|
||||
|
||||
func TestIsRuleKubescapeVersionCompatible(t *testing.T) {
|
||||
// local build- no build number
|
||||
|
||||
// should not crash when the value of useUntilKubescapeVersion is not a string
|
||||
buildNumberMock := "v1.0.135"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_invalid_from.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_invalid_until.Attributes, buildNumberMock))
|
||||
// should use only rules that don't have "until"
|
||||
buildNumberMock = ""
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.130"
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.132"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.133"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
|
||||
// should only use rules that version is in range of use
|
||||
buildNumberMock = "v1.0.135"
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock))
|
||||
assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock))
|
||||
assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock))
|
||||
}
|
||||
|
||||
func TestCheckLatestVersion_Semver_Compare(t *testing.T) {
|
||||
assert.Equal(t, -1, semver.Compare("v2.0.150", "v2.0.151"))
|
||||
assert.Equal(t, 0, semver.Compare("v2.0.150", "v2.0.150"))
|
||||
assert.Equal(t, 1, semver.Compare("v2.0.150", "v2.0.149"))
|
||||
assert.Equal(t, -1, semver.Compare("v2.0.150", "v3.0.150"))
|
||||
|
||||
}
|
||||
|
||||
func TestCheckLatestVersion(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
versionData *VersionCheckRequest
|
||||
versionURL string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Get latest version",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
versionData: &VersionCheckRequest{},
|
||||
versionURL: "https://us-central1-elated-pottery-310110.cloudfunctions.net/ksgf1v1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "Failed to get latest version",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
versionData: &VersionCheckRequest{},
|
||||
versionURL: "https://example.com",
|
||||
},
|
||||
err: fmt.Errorf("failed to get latest version"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &VersionCheckHandler{
|
||||
versionURL: tt.args.versionURL,
|
||||
}
|
||||
err := v.CheckLatestVersion(tt.args.ctx, tt.args.versionData)
|
||||
|
||||
assert.Equal(t, tt.err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionCheckHandler_getLatestVersion(t *testing.T) {
|
||||
type fields struct {
|
||||
versionURL string
|
||||
}
|
||||
type args struct {
|
||||
versionData *VersionCheckRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *VersionCheckResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Get latest version",
|
||||
fields: fields{
|
||||
versionURL: "https://us-central1-elated-pottery-310110.cloudfunctions.net/ksgf1v1",
|
||||
},
|
||||
args: args{
|
||||
versionData: &VersionCheckRequest{
|
||||
Client: "kubescape",
|
||||
},
|
||||
},
|
||||
want: &VersionCheckResponse{
|
||||
Client: "kubescape",
|
||||
ClientUpdate: "v3.0.0",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Failed to get latest version",
|
||||
fields: fields{
|
||||
versionURL: "https://example.com",
|
||||
},
|
||||
args: args{
|
||||
versionData: &VersionCheckRequest{},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &VersionCheckHandler{
|
||||
versionURL: tt.fields.versionURL,
|
||||
}
|
||||
got, err := v.getLatestVersion(tt.args.versionData)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("VersionCheckHandler.getLatestVersion() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("VersionCheckHandler.getLatestVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTriggerSource(t *testing.T) {
|
||||
// Running in github actions pipeline
|
||||
os.Setenv("GITHUB_ACTIONS", "true")
|
||||
source := getTriggerSource()
|
||||
assert.Equal(t, "pipeline", source)
|
||||
|
||||
os.Args[0] = "ksserver"
|
||||
source = getTriggerSource()
|
||||
assert.Equal(t, "microservice", source)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
@@ -35,8 +34,8 @@ func (ks *Kubescape) ViewCachedConfig(viewConfig *metav1.ViewConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ks *Kubescape) DeleteCachedConfig(ctx context.Context, deleteConfig *metav1.DeleteConfig) error {
|
||||
func (ks *Kubescape) DeleteCachedConfig(deleteConfig *metav1.DeleteConfig) error {
|
||||
|
||||
tenant := cautils.GetTenantConfig("", "", "", "", nil) // change k8sinterface
|
||||
return tenant.DeleteCachedConfig(ctx)
|
||||
return tenant.DeleteCachedConfig(ks.Context())
|
||||
}
|
||||
|
||||
@@ -83,7 +83,10 @@ func (a *OperatorAdapter) httpPostOperatorScanRequest(body apis.Commands) (strin
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return httputils.HttpRespToString(resp)
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("http-error: %d", resp.StatusCode)
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (a *OperatorAdapter) OperatorScan() (string, error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils/getter"
|
||||
@@ -44,12 +44,12 @@ func DownloadSupportCommands() []string {
|
||||
return commands
|
||||
}
|
||||
|
||||
func (ks *Kubescape) Download(ctx context.Context, downloadInfo *metav1.DownloadInfo) error {
|
||||
func (ks *Kubescape) Download(downloadInfo *metav1.DownloadInfo) error {
|
||||
setPathAndFilename(downloadInfo)
|
||||
if err := os.MkdirAll(downloadInfo.Path, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := downloadArtifact(ctx, downloadInfo, downloadFunc); err != nil {
|
||||
if err := downloadArtifact(ks.Context(), downloadInfo, downloadFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/fixhandler"
|
||||
)
|
||||
|
||||
@@ -17,14 +15,14 @@ const (
|
||||
confirmationQuestion = "Would you like to apply the changes to the files above? [y|n]: "
|
||||
)
|
||||
|
||||
func (ks *Kubescape) Fix(ctx context.Context, fixInfo *metav1.FixInfo) error {
|
||||
func (ks *Kubescape) Fix(fixInfo *metav1.FixInfo) error {
|
||||
logger.L().Info("Reading report file...")
|
||||
handler, err := fixhandler.NewFixHandler(fixInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourcesToFix := handler.PrepareResourcesToFix(ctx)
|
||||
resourcesToFix := handler.PrepareResourcesToFix(ks.Context())
|
||||
|
||||
if len(resourcesToFix) == 0 {
|
||||
logger.L().Info(noResourcesToFix)
|
||||
@@ -43,12 +41,12 @@ func (ks *Kubescape) Fix(ctx context.Context, fixInfo *metav1.FixInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
updatedFilesCount, errors := handler.ApplyChanges(ctx, resourcesToFix)
|
||||
updatedFilesCount, errors := handler.ApplyChanges(ks.Context(), resourcesToFix)
|
||||
logger.L().Info(fmt.Sprintf("Fixed resources in %d files.", updatedFilesCount))
|
||||
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
logger.L().Ctx(ctx).Warning(err.Error())
|
||||
logger.L().Ctx(ks.Context()).Warning(err.Error())
|
||||
}
|
||||
return fmt.Errorf("Failed to fix some resources, check the logs for more details")
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ func TestUserConfirmed(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.input), func(t *testing.T) {
|
||||
originalStdin := os.Stdin
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
defer func() {
|
||||
os.Stdin = os.Stdin
|
||||
os.Stdin = originalStdin
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -1,50 +1,212 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
ksmetav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling"
|
||||
"github.com/kubescape/kubescape/v3/pkg/imagescan"
|
||||
)
|
||||
|
||||
func (ks *Kubescape) ScanImage(ctx context.Context, imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
|
||||
// Data structure to represent attributes
|
||||
type Attributes struct {
|
||||
Registry string `json:"registry"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
ImageName string `json:"imageName"`
|
||||
ImageTag string `json:"imageTag,omitempty"`
|
||||
}
|
||||
|
||||
// Data structure for a target
|
||||
type Target struct {
|
||||
DesignatorType string `json:"designatorType"`
|
||||
Attributes Attributes `json:"attributes"`
|
||||
}
|
||||
|
||||
// Data structure for metadata
|
||||
type Metadata struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Data structure for vulnerabilities and severities
|
||||
type VulnerabilitiesIgnorePolicy struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
Kind string `json:"kind"`
|
||||
Targets []Target `json:"targets"`
|
||||
Vulnerabilities []string `json:"vulnerabilities"`
|
||||
Severities []string `json:"severities"`
|
||||
}
|
||||
|
||||
// Loads excpetion policies from exceptions json object.
|
||||
func GetImageExceptionsFromFile(filePath string) ([]VulnerabilitiesIgnorePolicy, error) {
|
||||
// Read the JSON file
|
||||
jsonFile, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading exceptions file: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal the JSON data into an array of VulnerabilitiesIgnorePolicy
|
||||
var policies []VulnerabilitiesIgnorePolicy
|
||||
err = json.Unmarshal(jsonFile, &policies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling exceptions file: %w", err)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// This function will identify the registry, organization and image tag from the image name
|
||||
func getAttributesFromImage(imgName string) (Attributes, error) {
|
||||
canonicalImageName, err := cautils.NormalizeImageName(imgName)
|
||||
if err != nil {
|
||||
return Attributes{}, err
|
||||
}
|
||||
|
||||
tokens := strings.Split(canonicalImageName, "/")
|
||||
registry := tokens[0]
|
||||
organization := tokens[1]
|
||||
|
||||
imageNameAndTag := strings.Split(tokens[2], ":")
|
||||
imageName := imageNameAndTag[0]
|
||||
|
||||
// Intialize the image tag with default value
|
||||
imageTag := "latest"
|
||||
if len(imageNameAndTag) > 1 {
|
||||
imageTag = imageNameAndTag[1]
|
||||
}
|
||||
|
||||
attributes := Attributes{
|
||||
Registry: registry,
|
||||
Organization: organization,
|
||||
ImageName: imageName,
|
||||
ImageTag: imageTag,
|
||||
}
|
||||
|
||||
return attributes, nil
|
||||
}
|
||||
|
||||
// Checks if the target string matches the regex pattern
|
||||
func regexStringMatch(pattern, target string) bool {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to generate regular expression: %s", err))
|
||||
return false
|
||||
}
|
||||
|
||||
if re.MatchString(target) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Compares the registry, organization, image name, image tag against the targets specified
|
||||
// in the exception policy object to check if the image being scanned qualifies for an
|
||||
// exception policy.
|
||||
func isTargetImage(targets []Target, attributes Attributes) bool {
|
||||
for _, target := range targets {
|
||||
if regexStringMatch(target.Attributes.Registry, attributes.Registry) && regexStringMatch(target.Attributes.Organization, attributes.Organization) && regexStringMatch(target.Attributes.ImageName, attributes.ImageName) && regexStringMatch(target.Attributes.ImageTag, attributes.ImageTag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Generates a list of unique CVE-IDs and the severities which are to be excluded for
|
||||
// the image being scanned.
|
||||
func getUniqueVulnerabilitiesAndSeverities(policies []VulnerabilitiesIgnorePolicy, image string) ([]string, []string) {
|
||||
// Create maps with slices as values to store unique vulnerabilities and severities (case-insensitive)
|
||||
uniqueVulns := make(map[string][]string)
|
||||
uniqueSevers := make(map[string][]string)
|
||||
|
||||
imageAttributes, err := getAttributesFromImage(image)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to generate image attributes: %s", err))
|
||||
}
|
||||
|
||||
// Iterate over each policy and its vulnerabilities/severities
|
||||
for _, policy := range policies {
|
||||
// Include the exceptions only if the image is one of the targets
|
||||
if isTargetImage(policy.Targets, imageAttributes) {
|
||||
for _, vulnerability := range policy.Vulnerabilities {
|
||||
// Add to slice directly
|
||||
vulnerabilityUppercase := strings.ToUpper(vulnerability)
|
||||
uniqueVulns[vulnerabilityUppercase] = append(uniqueVulns[vulnerabilityUppercase], vulnerability)
|
||||
}
|
||||
|
||||
for _, severity := range policy.Severities {
|
||||
// Add to slice directly
|
||||
severityUppercase := strings.ToUpper(severity)
|
||||
uniqueSevers[severityUppercase] = append(uniqueSevers[severityUppercase], severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract unique keys (which are unique vulnerabilities/severities) and their slices
|
||||
uniqueVulnsList := make([]string, 0, len(uniqueVulns))
|
||||
for vuln := range uniqueVulns {
|
||||
uniqueVulnsList = append(uniqueVulnsList, vuln)
|
||||
}
|
||||
|
||||
uniqueSeversList := make([]string, 0, len(uniqueSevers))
|
||||
for sever := range uniqueSevers {
|
||||
uniqueSeversList = append(uniqueSeversList, sever)
|
||||
}
|
||||
|
||||
return uniqueVulnsList, uniqueSeversList
|
||||
}
|
||||
|
||||
func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (bool, error) {
|
||||
logger.L().Start(fmt.Sprintf("Scanning image %s...", imgScanInfo.Image))
|
||||
|
||||
dbCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc := imagescan.NewScanService(dbCfg)
|
||||
distCfg, installCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc, err := imagescan.NewScanServiceWithMatchers(distCfg, installCfg, imgScanInfo.UseDefaultMatchers)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
|
||||
return false, err
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
creds := imagescan.RegistryCredentials{
|
||||
Username: imgScanInfo.Username,
|
||||
Password: imgScanInfo.Password,
|
||||
}
|
||||
|
||||
scanResults, err := svc.Scan(ctx, imgScanInfo.Image, creds)
|
||||
var vulnerabilityExceptions []string
|
||||
var severityExceptions []string
|
||||
if imgScanInfo.Exceptions != "" {
|
||||
exceptionPolicies, err := GetImageExceptionsFromFile(imgScanInfo.Exceptions)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to load exceptions from file: %s", imgScanInfo.Exceptions))
|
||||
return false, err
|
||||
}
|
||||
|
||||
vulnerabilityExceptions, severityExceptions = getUniqueVulnerabilitiesAndSeverities(exceptionPolicies, imgScanInfo.Image)
|
||||
}
|
||||
|
||||
imageScanData, err := svc.Scan(ks.Context(), imgScanInfo.Image, creds, vulnerabilityExceptions, severityExceptions)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to scan image: %s", imgScanInfo.Image))
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", imgScanInfo.Image))
|
||||
|
||||
scanInfo.SetScanType(cautils.ScanTypeImage)
|
||||
|
||||
outputPrinters := GetOutputPrinters(scanInfo, ctx, "")
|
||||
outputPrinters := GetOutputPrinters(scanInfo, ks.Context(), "")
|
||||
|
||||
uiPrinter := GetUIPrinter(ctx, scanInfo, "")
|
||||
uiPrinter := GetUIPrinter(ks.Context(), scanInfo, "")
|
||||
|
||||
resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter)
|
||||
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{
|
||||
{
|
||||
PresenterConfig: scanResults,
|
||||
Image: imgScanInfo.Image,
|
||||
},
|
||||
}
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{*imageScanData}
|
||||
|
||||
return scanResults, resultsHandler.HandleResults(ctx)
|
||||
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), imageScanData.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
|
||||
}
|
||||
|
||||
447
core/core/image_scan_test.go
Normal file
447
core/core/image_scan_test.go
Normal file
@@ -0,0 +1,447 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetImageExceptionsFromFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
filePath string
|
||||
expectedPolicies []VulnerabilitiesIgnorePolicy
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
filePath: "./testdata/exceptions.json",
|
||||
expectedPolicies: []VulnerabilitiesIgnorePolicy{
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "medium-severity-vulnerabilites-exceptions",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{},
|
||||
Severities: []string{"medium"},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "exclude-allowed-hostPath-control",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "",
|
||||
Organization: "",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
|
||||
Severities: []string{"critical", "low"},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "regex-example",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "quay.*",
|
||||
Organization: "kube*",
|
||||
ImageName: "kubescape*",
|
||||
ImageTag: "v2*",
|
||||
},
|
||||
},
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: ".*",
|
||||
ImageName: "kube*",
|
||||
ImageTag: "v3*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-6879", "CVE-2023-44487"},
|
||||
Severities: []string{"critical", "low"},
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
filePath: "./testdata/empty_exceptions.json",
|
||||
expectedPolicies: []VulnerabilitiesIgnorePolicy{},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filePath, func(t *testing.T) {
|
||||
policies, err := GetImageExceptionsFromFile(tt.filePath)
|
||||
assert.Equal(t, tt.expectedPolicies, policies)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAttributesFromImage(t *testing.T) {
|
||||
tests := []struct {
|
||||
imageName string
|
||||
expectedAttributes Attributes
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
imageName: "quay.io/kubescape/kubescape-cli:v3.0.0",
|
||||
expectedAttributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kubescape",
|
||||
ImageName: "kubescape-cli",
|
||||
ImageTag: "v3.0.0",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
imageName: "alpine",
|
||||
expectedAttributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "library",
|
||||
ImageName: "alpine",
|
||||
ImageTag: "latest",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.imageName, func(t *testing.T) {
|
||||
attributes, err := getAttributesFromImage(tt.imageName)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
assert.Equal(t, tt.expectedAttributes, attributes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexStringMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
pattern string
|
||||
target string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
pattern: ".*",
|
||||
target: "quay.io",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pattern: "kubescape",
|
||||
target: "kubescape",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pattern: "kubescape*",
|
||||
target: "kubescape-cli",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pattern: "",
|
||||
target: "v3.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pattern: "docker.io",
|
||||
target: "quay.io",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.target+"/"+tt.pattern, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, regexStringMatch(tt.pattern, tt.target))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTargetImage(t *testing.T) {
|
||||
tests := []struct {
|
||||
targets []Target
|
||||
attributes Attributes
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
targets: []Target{
|
||||
{
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: ".*",
|
||||
ImageName: ".*",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kubescape",
|
||||
ImageName: "kubescape-cli",
|
||||
ImageTag: "v3.0.0",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
targets: []Target{
|
||||
{
|
||||
Attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kubescape",
|
||||
ImageName: "kubescape*",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kubescape",
|
||||
ImageName: "kubescape-cli",
|
||||
ImageTag: "v3.0.0",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
targets: []Target{
|
||||
{
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "library",
|
||||
ImageName: "alpine",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "library",
|
||||
ImageName: "alpine",
|
||||
ImageTag: "latest",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
targets: []Target{
|
||||
{
|
||||
Attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kubescape",
|
||||
ImageName: "kubescape*",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "library",
|
||||
ImageName: "alpine",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "library",
|
||||
ImageName: "alpine",
|
||||
ImageTag: "latest",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.attributes.Registry+"/"+tt.attributes.ImageName, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, isTargetImage(tt.targets, tt.attributes))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVulnerabilitiesAndSeverities(t *testing.T) {
|
||||
tests := []struct {
|
||||
policies []VulnerabilitiesIgnorePolicy
|
||||
image string
|
||||
expectedVulnerabilities []string
|
||||
expectedSeverities []string
|
||||
}{
|
||||
{
|
||||
policies: []VulnerabilitiesIgnorePolicy{
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "vulnerabilites-exceptions",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "",
|
||||
Organization: "kubescape*",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-42365"},
|
||||
Severities: []string{},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "exclude-allowed-hostPath-control",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
Organization: "",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
|
||||
Severities: []string{"critical", "low"},
|
||||
},
|
||||
},
|
||||
image: "quay.io/kubescape/kubescape-cli:v3.0.0",
|
||||
expectedVulnerabilities: []string{"CVE-2023-42365"},
|
||||
expectedSeverities: []string{},
|
||||
},
|
||||
{
|
||||
policies: []VulnerabilitiesIgnorePolicy{
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "medium-severity-vulnerabilites-exceptions",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "",
|
||||
Organization: "",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{},
|
||||
Severities: []string{"medium"},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "exclude-allowed-hostPath-control",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
|
||||
Severities: []string{},
|
||||
},
|
||||
},
|
||||
image: "alpine",
|
||||
expectedVulnerabilities: []string{},
|
||||
expectedSeverities: []string{"MEDIUM"},
|
||||
},
|
||||
{
|
||||
policies: []VulnerabilitiesIgnorePolicy{
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "regex-example",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "quay.io",
|
||||
Organization: "kube*",
|
||||
ImageName: "kubescape*",
|
||||
ImageTag: ".*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{},
|
||||
Severities: []string{"critical"},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "only-for-docker-registry",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
Registry: "docker.io",
|
||||
ImageTag: "v3*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2022-28391"},
|
||||
Severities: []string{"high"},
|
||||
},
|
||||
{
|
||||
Metadata: Metadata{
|
||||
Name: "exclude-allowed-hostPath-control",
|
||||
},
|
||||
Kind: "VulnerabilitiesIgnorePolicy",
|
||||
Targets: []Target{
|
||||
{
|
||||
DesignatorType: "Attributes",
|
||||
Attributes: Attributes{
|
||||
ImageTag: "v3*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2022-30065", "CVE-2022-28391"},
|
||||
Severities: []string{},
|
||||
},
|
||||
},
|
||||
image: "quay.io/kubescape/kubescape-cli:v3.0.0",
|
||||
expectedVulnerabilities: []string{"CVE-2022-30065", "CVE-2022-28391"},
|
||||
expectedSeverities: []string{"CRITICAL"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.image, func(t *testing.T) {
|
||||
vulnerabilities, severities := getUniqueVulnerabilitiesAndSeverities(tt.policies, tt.image)
|
||||
sort.Strings(tt.expectedVulnerabilities)
|
||||
sort.Strings(vulnerabilities)
|
||||
assert.Equal(t, tt.expectedVulnerabilities, vulnerabilities)
|
||||
assert.Equal(t, tt.expectedSeverities, severities)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
@@ -16,11 +17,8 @@ import (
|
||||
printerv2 "github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter"
|
||||
reporterv2 "github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter/v2"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/kubescape/rbac-utils/rbacscanner"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
// getKubernetesApi
|
||||
@@ -92,7 +90,11 @@ func getResourceHandler(ctx context.Context, scanInfo *cautils.ScanInfo, tenantC
|
||||
return resourcehandler.NewFileResourceHandler()
|
||||
}
|
||||
|
||||
getter.GetKSCloudAPIConnector()
|
||||
// Only initialize cloud connector if not in air-gapped mode
|
||||
// This call initializes the global cloud API connector for later use
|
||||
if !isAirGappedMode(scanInfo) {
|
||||
_ = getter.GetKSCloudAPIConnector()
|
||||
}
|
||||
rbacObjects := getRBACHandler(tenantConfig, k8s, scanInfo.Submit)
|
||||
return resourcehandler.NewK8sResourceHandler(k8s, hostSensorHandler, rbacObjects, tenantConfig.GetContextName())
|
||||
}
|
||||
@@ -277,7 +279,7 @@ func getAttackTracksGetter(ctx context.Context, attackTracks, accountID string,
|
||||
return downloadReleasedPolicy
|
||||
}
|
||||
|
||||
// getUIPrinter returns a printer that will be used to print to the program’s UI (terminal)
|
||||
// GetUIPrinter returns a printer that will be used to print to the program’s UI (terminal)
|
||||
func GetUIPrinter(ctx context.Context, scanInfo *cautils.ScanInfo, clusterName string) printer.IPrinter {
|
||||
var p printer.IPrinter
|
||||
if helpers.ToLevel(logger.L().GetLevel()) >= helpers.WarningLevel {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
package core
|
||||
|
||||
type Kubescape struct{}
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func NewKubescape() *Kubescape {
|
||||
return &Kubescape{}
|
||||
type Kubescape struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func (ks *Kubescape) Context() context.Context {
|
||||
return ks.Ctx
|
||||
}
|
||||
|
||||
func NewKubescape(ctx context.Context) *Kubescape {
|
||||
return &Kubescape{Ctx: ctx}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -8,7 +9,8 @@ import (
|
||||
|
||||
// The function should return a non-nil pointer.
|
||||
func TestNewKubescape_ReturnsNonNilPointer(t *testing.T) {
|
||||
k := NewKubescape()
|
||||
ctx := context.TODO()
|
||||
k := NewKubescape(ctx)
|
||||
assert.NotNil(t, k)
|
||||
}
|
||||
|
||||
@@ -19,5 +21,6 @@ func TestNewKubescape_DoesNotPanic(t *testing.T) {
|
||||
t.Errorf("Function panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
NewKubescape()
|
||||
ctx := context.TODO()
|
||||
NewKubescape(ctx)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,13 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer"
|
||||
v2 "github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/maruel/natural"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
var listFunc = map[string]func(context.Context, *metav1.ListPolicies) ([]string, error){
|
||||
@@ -38,16 +37,16 @@ func ListSupportActions() []string {
|
||||
sort.Strings(commands)
|
||||
return commands
|
||||
}
|
||||
func (ks *Kubescape) List(ctx context.Context, listPolicies *metav1.ListPolicies) error {
|
||||
func (ks *Kubescape) List(listPolicies *metav1.ListPolicies) error {
|
||||
if policyListerFunc, ok := listFunc[listPolicies.Target]; ok {
|
||||
policies, err := policyListerFunc(ctx, listPolicies)
|
||||
policies, err := policyListerFunc(ks.Context(), listPolicies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies = naturalSortPolicies(policies)
|
||||
|
||||
if listFormatFunction, ok := listFormatFunc[listPolicies.Format]; ok {
|
||||
listFormatFunction(ctx, listPolicies.Target, policies)
|
||||
listFormatFunction(ks.Context(), listPolicies.Target, policies)
|
||||
} else {
|
||||
return fmt.Errorf("Invalid format \"%s\", Supported formats: 'pretty-print'/'json' ", listPolicies.Format)
|
||||
}
|
||||
@@ -100,30 +99,19 @@ func prettyPrintListFormat(ctx context.Context, targetPolicy string, policies []
|
||||
return
|
||||
}
|
||||
|
||||
policyTable := tablewriter.NewWriter(printer.GetWriter(ctx, ""))
|
||||
policyTable := table.NewWriter()
|
||||
policyTable.SetOutputMirror(printer.GetWriter(ctx, ""))
|
||||
|
||||
policyTable.SetAutoWrapText(true)
|
||||
header := fmt.Sprintf("Supported %s", targetPolicy)
|
||||
policyTable.SetHeader([]string{header})
|
||||
policyTable.SetHeaderLine(true)
|
||||
policyTable.SetRowLine(true)
|
||||
policyTable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
policyTable.SetAutoFormatHeaders(false)
|
||||
policyTable.SetAlignment(tablewriter.ALIGN_CENTER)
|
||||
policyTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
data := v2.Matrix{}
|
||||
policyTable.AppendHeader(table.Row{header})
|
||||
policyTable.Style().Options.SeparateHeader = true
|
||||
policyTable.Style().Options.SeparateRows = true
|
||||
policyTable.Style().Format.HeaderAlign = text.AlignLeft
|
||||
policyTable.Style().Format.Header = text.FormatDefault
|
||||
policyTable.Style().Format.RowAlign = text.AlignCenter
|
||||
policyTable.Style().Box = table.StyleBoxRounded
|
||||
|
||||
controlRows := generatePolicyRows(policies)
|
||||
|
||||
var headerColors []tablewriter.Colors
|
||||
for range controlRows[0] {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor})
|
||||
}
|
||||
policyTable.SetHeaderColor(headerColors...)
|
||||
|
||||
data = append(data, controlRows...)
|
||||
|
||||
policyTable.AppendBulk(data)
|
||||
policyTable.AppendRows(generatePolicyRows(policies))
|
||||
policyTable.Render()
|
||||
}
|
||||
|
||||
@@ -134,40 +122,32 @@ func jsonListFormat(_ context.Context, _ string, policies []string) {
|
||||
}
|
||||
|
||||
func prettyPrintControls(ctx context.Context, policies []string) {
|
||||
controlsTable := tablewriter.NewWriter(printer.GetWriter(ctx, ""))
|
||||
controlsTable := table.NewWriter()
|
||||
controlsTable.SetOutputMirror(printer.GetWriter(ctx, ""))
|
||||
|
||||
controlsTable.SetAutoWrapText(false)
|
||||
controlsTable.SetHeaderLine(true)
|
||||
controlsTable.SetRowLine(true)
|
||||
controlsTable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
controlsTable.SetAutoFormatHeaders(false)
|
||||
controlsTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
controlsTable.Style().Options.SeparateHeader = true
|
||||
controlsTable.Style().Options.SeparateRows = true
|
||||
controlsTable.Style().Format.HeaderAlign = text.AlignLeft
|
||||
controlsTable.Style().Format.Header = text.FormatDefault
|
||||
controlsTable.Style().Box = table.StyleBoxRounded
|
||||
controlsTable.SetColumnConfigs([]table.ColumnConfig{{Number: 1, Align: text.AlignRight}})
|
||||
|
||||
controlRows := generateControlRows(policies)
|
||||
|
||||
short := utils.CheckShortTerminalWidth(controlRows, []string{"Control ID", "Control name", "Docs", "Frameworks"})
|
||||
short := utils.CheckShortTerminalWidth(controlRows, table.Row{"Control ID", "Control name", "Docs", "Frameworks"})
|
||||
if short {
|
||||
controlsTable.SetAutoWrapText(false)
|
||||
controlsTable.SetHeader([]string{"Controls"})
|
||||
controlsTable.AppendHeader(table.Row{"Controls"})
|
||||
controlRows = shortFormatControlRows(controlRows)
|
||||
} else {
|
||||
controlsTable.SetHeader([]string{"Control ID", "Control name", "Docs", "Frameworks"})
|
||||
controlsTable.AppendHeader(table.Row{"Control ID", "Control name", "Docs", "Frameworks"})
|
||||
}
|
||||
var headerColors []tablewriter.Colors
|
||||
for range controlRows[0] {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor})
|
||||
}
|
||||
controlsTable.SetHeaderColor(headerColors...)
|
||||
|
||||
data := v2.Matrix{}
|
||||
data = append(data, controlRows...)
|
||||
|
||||
controlsTable.AppendBulk(data)
|
||||
controlsTable.AppendRows(controlRows)
|
||||
controlsTable.Render()
|
||||
}
|
||||
|
||||
func generateControlRows(policies []string) [][]string {
|
||||
rows := [][]string{}
|
||||
func generateControlRows(policies []string) []table.Row {
|
||||
rows := make([]table.Row, 0, len(policies))
|
||||
|
||||
for _, control := range policies {
|
||||
|
||||
@@ -188,7 +168,7 @@ func generateControlRows(policies []string) [][]string {
|
||||
|
||||
docs := cautils.GetControlLink(id)
|
||||
|
||||
currentRow := []string{id, control, docs, strings.Replace(framework, " ", "\n", -1)}
|
||||
currentRow := table.Row{id, control, docs, strings.Replace(framework, " ", "\n", -1)}
|
||||
|
||||
rows = append(rows, currentRow)
|
||||
}
|
||||
@@ -196,20 +176,19 @@ func generateControlRows(policies []string) [][]string {
|
||||
return rows
|
||||
}
|
||||
|
||||
func generatePolicyRows(policies []string) [][]string {
|
||||
rows := [][]string{}
|
||||
func generatePolicyRows(policies []string) []table.Row {
|
||||
rows := make([]table.Row, 0, len(policies))
|
||||
|
||||
for _, policy := range policies {
|
||||
currentRow := []string{policy}
|
||||
rows = append(rows, currentRow)
|
||||
rows = append(rows, table.Row{policy})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func shortFormatControlRows(controlRows [][]string) [][]string {
|
||||
rows := [][]string{}
|
||||
func shortFormatControlRows(controlRows []table.Row) []table.Row {
|
||||
rows := make([]table.Row, 0, len(controlRows))
|
||||
for _, controlRow := range controlRows {
|
||||
rows = append(rows, []string{fmt.Sprintf("Control ID"+strings.Repeat(" ", 3)+": %+v\nControl Name"+strings.Repeat(" ", 1)+": %+v\nDocs"+strings.Repeat(" ", 9)+": %+v\nFrameworks"+strings.Repeat(" ", 3)+": %+v", controlRow[0], controlRow[1], controlRow[2], strings.Replace(controlRow[3], "\n", " ", -1))})
|
||||
rows = append(rows, table.Row{fmt.Sprintf("Control ID"+strings.Repeat(" ", 3)+": %+v\nControl Name"+strings.Repeat(" ", 1)+": %+v\nDocs"+strings.Repeat(" ", 9)+": %+v\nFrameworks"+strings.Repeat(" ", 3)+": %+v", controlRow[0], controlRow[1], controlRow[2], strings.Replace(controlRow[3].(string), "\n", " ", -1))})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -105,7 +106,7 @@ func TestGeneratePolicyRows_NonEmptyPolicyList(t *testing.T) {
|
||||
result := generatePolicyRows(policies)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, [][]string{{"policy1"}, {"policy2"}, {"policy3"}}, result)
|
||||
assert.Equal(t, []table.Row{{"policy1"}, {"policy2"}, {"policy3"}}, result)
|
||||
}
|
||||
|
||||
// Returns an empty 2D slice for an empty list of policies.
|
||||
@@ -122,12 +123,12 @@ func TestGeneratePolicyRows_EmptyPolicyList(t *testing.T) {
|
||||
|
||||
// The function returns a list of rows, each containing a formatted string with control ID, control name, docs, and frameworks.
|
||||
func TestShortFormatControlRows_ReturnsListOfRowsWithFormattedString(t *testing.T) {
|
||||
controlRows := [][]string{
|
||||
controlRows := []table.Row{
|
||||
{"ID1", "Control 1", "Docs 1", "Framework 1"},
|
||||
{"ID2", "Control 2", "Docs 2", "Framework 2"},
|
||||
}
|
||||
|
||||
want := [][]string{
|
||||
want := []table.Row{
|
||||
{"Control ID : ID1\nControl Name : Control 1\nDocs : Docs 1\nFrameworks : Framework 1"},
|
||||
{"Control ID : ID2\nControl Name : Control 2\nDocs : Docs 2\nFrameworks : Framework 2"},
|
||||
}
|
||||
@@ -139,12 +140,12 @@ func TestShortFormatControlRows_ReturnsListOfRowsWithFormattedString(t *testing.
|
||||
|
||||
// The function formats the control rows correctly, replacing newlines in the frameworks column with line breaks.
|
||||
func TestShortFormatControlRows_FormatsControlRowsCorrectly(t *testing.T) {
|
||||
controlRows := [][]string{
|
||||
controlRows := []table.Row{
|
||||
{"ID1", "Control 1", "Docs 1", "Framework\n1"},
|
||||
{"ID2", "Control 2", "Docs 2", "Framework\n2"},
|
||||
}
|
||||
|
||||
want := [][]string{
|
||||
want := []table.Row{
|
||||
{"Control ID : ID1\nControl Name : Control 1\nDocs : Docs 1\nFrameworks : Framework 1"},
|
||||
{"Control ID : ID2\nControl Name : Control 2\nDocs : Docs 2\nFrameworks : Framework 2"},
|
||||
}
|
||||
@@ -156,11 +157,11 @@ func TestShortFormatControlRows_FormatsControlRowsCorrectly(t *testing.T) {
|
||||
|
||||
// The function handles a control row with an empty control ID.
|
||||
func TestShortFormatControlRows_HandlesControlRowWithEmptyControlID(t *testing.T) {
|
||||
controlRows := [][]string{
|
||||
controlRows := []table.Row{
|
||||
{"", "Control 1", "Docs 1", "Framework 1"},
|
||||
}
|
||||
|
||||
want := [][]string{
|
||||
want := []table.Row{
|
||||
{"Control ID : \nControl Name : Control 1\nDocs : Docs 1\nFrameworks : Framework 1"},
|
||||
}
|
||||
|
||||
@@ -171,11 +172,11 @@ func TestShortFormatControlRows_HandlesControlRowWithEmptyControlID(t *testing.T
|
||||
|
||||
// The function handles a control row with an empty control name.
|
||||
func TestShortFormatControlRows_HandlesControlRowWithEmptyControlName(t *testing.T) {
|
||||
controlRows := [][]string{
|
||||
controlRows := []table.Row{
|
||||
{"ID1", "", "Docs 1", "Framework 1"},
|
||||
}
|
||||
|
||||
want := [][]string{
|
||||
want := []table.Row{
|
||||
{"Control ID : ID1\nControl Name : \nDocs : Docs 1\nFrameworks : Framework 1"},
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ func TestGenerateControlRowsWithAllFields(t *testing.T) {
|
||||
"3|Control 3|Framework 3",
|
||||
}
|
||||
|
||||
want := [][]string{
|
||||
want := []table.Row{
|
||||
{"1", "Control 1", "https://hub.armosec.io/docs/1", "Framework\n1"},
|
||||
{"2", "Control 2", "https://hub.armosec.io/docs/2", "Framework\n2"},
|
||||
{"3", "Control 3", "https://hub.armosec.io/docs/3", "Framework\n3"},
|
||||
@@ -215,7 +216,7 @@ func TestGenerateControlRowsHandlesPoliciesWithEmptyStringOrNoPipesOrOnePipeMiss
|
||||
"5|Control 5||Extra 5",
|
||||
}
|
||||
|
||||
expectedRows := [][]string{
|
||||
expectedRows := []table.Row{
|
||||
{"", "", "https://hub.armosec.io/docs/", ""},
|
||||
{"1", "", "https://hub.armosec.io/docs/1", ""},
|
||||
{"2", "Control 2", "https://hub.armosec.io/docs/2", "Framework\n2"},
|
||||
@@ -252,18 +253,18 @@ func TestGenerateTableWithCorrectHeadersAndRows(t *testing.T) {
|
||||
os.Stdout = rescueStdout
|
||||
|
||||
// got := buf.String()
|
||||
want := `┌────────────┬──────────────┬───────────────────────────────┬────────────┐
|
||||
want := `╭────────────┬──────────────┬───────────────────────────────┬────────────╮
|
||||
│ Control ID │ Control name │ Docs │ Frameworks │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 1 │ Control 1 │ https://hub.armosec.io/docs/1 │ Framework │
|
||||
│ │ │ │ 1 │
|
||||
│ │ │ │ 1 │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 2 │ Control 2 │ https://hub.armosec.io/docs/2 │ Framework │
|
||||
│ │ │ │ 2 │
|
||||
│ │ │ │ 2 │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 3 │ Control 3 │ https://hub.armosec.io/docs/3 │ Framework │
|
||||
│ │ │ │ 3 │
|
||||
└────────────┴──────────────┴───────────────────────────────┴────────────┘
|
||||
│ │ │ │ 3 │
|
||||
╰────────────┴──────────────┴───────────────────────────────┴────────────╯
|
||||
`
|
||||
|
||||
assert.Equal(t, want, string(got))
|
||||
@@ -294,7 +295,7 @@ func TestGenerateTableWithMalformedPoliciesAndPrettyPrintHeadersAndRows(t *testi
|
||||
|
||||
os.Stdout = rescueStdout
|
||||
|
||||
want := `┌────────────┬──────────────┬───────────────────────────────┬────────────┐
|
||||
want := `╭────────────┬──────────────┬───────────────────────────────┬────────────╮
|
||||
│ Control ID │ Control name │ Docs │ Frameworks │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ │ │ https://hub.armosec.io/docs/ │ │
|
||||
@@ -302,18 +303,18 @@ func TestGenerateTableWithMalformedPoliciesAndPrettyPrintHeadersAndRows(t *testi
|
||||
│ 1 │ │ https://hub.armosec.io/docs/1 │ │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 2 │ Control 2 │ https://hub.armosec.io/docs/2 │ Framework │
|
||||
│ │ │ │ 2 │
|
||||
│ │ │ │ 2 │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 3 │ Control 3 │ https://hub.armosec.io/docs/3 │ Framework │
|
||||
│ │ │ │ 3 │
|
||||
│ │ │ │ 3 │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 4 │ │ https://hub.armosec.io/docs/4 │ Framework │
|
||||
│ │ │ │ 4 │
|
||||
│ │ │ │ 4 │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ │ │ https://hub.armosec.io/docs/ │ │
|
||||
├────────────┼──────────────┼───────────────────────────────┼────────────┤
|
||||
│ 5 │ Control 5 │ https://hub.armosec.io/docs/5 │ │
|
||||
└────────────┴──────────────┴───────────────────────────────┴────────────┘
|
||||
╰────────────┴──────────────┴───────────────────────────────┴────────────╯
|
||||
`
|
||||
|
||||
assert.Equal(t, want, string(got))
|
||||
|
||||
@@ -1,81 +1,118 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/clio"
|
||||
grypejson "github.com/anchore/grype/grype/presenter/json"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
logger "github.com/kubescape/go-logger"
|
||||
copaGrype "github.com/anubhav06/copa-grype/grype"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
ksmetav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer"
|
||||
"github.com/kubescape/kubescape/v3/pkg/imagescan"
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling"
|
||||
copa "github.com/project-copacetic/copacetic/pkg/patch"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
gwclient "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
"github.com/project-copacetic/copacetic/pkg/buildkit"
|
||||
"github.com/project-copacetic/copacetic/pkg/pkgmgr"
|
||||
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
|
||||
"github.com/project-copacetic/copacetic/pkg/utils"
|
||||
"github.com/quay/claircore/osrelease"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
|
||||
const (
|
||||
copaProduct = "copa"
|
||||
)
|
||||
|
||||
func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.ScanInfo) (bool, error) {
|
||||
|
||||
// ===================== Scan the image =====================
|
||||
logger.L().Start(fmt.Sprintf("Scanning image: %s", patchInfo.Image))
|
||||
|
||||
// Setup the scan service
|
||||
dbCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc := imagescan.NewScanService(dbCfg)
|
||||
distCfg, installCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc, err := imagescan.NewScanServiceWithMatchers(distCfg, installCfg, scanInfo.UseDefaultMatchers)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
|
||||
return false, err
|
||||
}
|
||||
defer svc.Close()
|
||||
creds := imagescan.RegistryCredentials{
|
||||
Username: patchInfo.Username,
|
||||
Password: patchInfo.Password,
|
||||
}
|
||||
// Scan the image
|
||||
scanResults, err := svc.Scan(ctx, patchInfo.Image, creds)
|
||||
scanResults, err := svc.Scan(ks.Context(), patchInfo.Image, creds, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
model, err := models.NewDocument(clio.Identification{}, scanResults.Packages, scanResults.Context,
|
||||
*scanResults.RemainingMatches, scanResults.IgnoredMatches, scanResults.VulnerabilityProvider, nil, nil, models.DefaultSortStrategy, false)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create document: %w", err)
|
||||
}
|
||||
|
||||
// If the scan results ID is empty, set it to "grype"
|
||||
if model.Descriptor.Name == "" {
|
||||
model.Descriptor.Name = "grype"
|
||||
}
|
||||
// Save the scan results to a file in json format
|
||||
pres := presenter.GetPresenter("json", "", false, *scanResults)
|
||||
pres := grypejson.NewPresenter(models.PresenterConfig{Document: model, SBOM: scanResults.SBOM})
|
||||
|
||||
fileName := fmt.Sprintf("%s:%s.json", patchInfo.ImageName, patchInfo.ImageTag)
|
||||
fileName = strings.ReplaceAll(fileName, "/", "-")
|
||||
|
||||
writer := printer.GetWriter(ctx, fileName)
|
||||
writer := printer.GetWriter(ks.Context(), fileName)
|
||||
|
||||
if err := pres.Present(writer); err != nil {
|
||||
return nil, err
|
||||
if err = pres.Present(writer); err != nil {
|
||||
return false, err
|
||||
}
|
||||
logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", patchInfo.Image))
|
||||
|
||||
// ===================== Patch the image using copacetic =====================
|
||||
logger.L().Start("Patching image...")
|
||||
patchedImageName := fmt.Sprintf("%s:%s", patchInfo.ImageName, patchInfo.PatchedImageTag)
|
||||
|
||||
sout, serr := os.Stdout, os.Stderr
|
||||
if logger.L().GetLevel() != "debug" {
|
||||
disableCopaLogger()
|
||||
}
|
||||
|
||||
if err := copa.Patch(ctx, patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchInfo.PatchedImageTag, ""); err != nil {
|
||||
return nil, err
|
||||
if err = copaPatch(ks.Context(), patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchedImageName, "", patchInfo.IgnoreError, patchInfo.BuildKitOpts); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Restore the output streams
|
||||
os.Stdout, os.Stderr = sout, serr
|
||||
|
||||
patchedImageName := fmt.Sprintf("%s:%s", patchInfo.ImageName, patchInfo.PatchedImageTag)
|
||||
logger.L().StopSuccess(fmt.Sprintf("Patched image successfully. Loaded image: %s", patchedImageName))
|
||||
|
||||
// ===================== Re-scan the image =====================
|
||||
|
||||
logger.L().Start(fmt.Sprintf("Re-scanning image: %s", patchedImageName))
|
||||
|
||||
scanResultsPatched, err := svc.Scan(ctx, patchedImageName, creds)
|
||||
scanResultsPatched, err := svc.Scan(ks.Context(), patchedImageName, creds, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
logger.L().StopSuccess(fmt.Sprintf("Successfully re-scanned image: %s", patchedImageName))
|
||||
|
||||
@@ -88,18 +125,12 @@ func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo, s
|
||||
// ===================== Results Handling =====================
|
||||
|
||||
scanInfo.SetScanType(cautils.ScanTypeImage)
|
||||
outputPrinters := GetOutputPrinters(scanInfo, ctx, "")
|
||||
uiPrinter := GetUIPrinter(ctx, scanInfo, "")
|
||||
outputPrinters := GetOutputPrinters(scanInfo, ks.Context(), "")
|
||||
uiPrinter := GetUIPrinter(ks.Context(), scanInfo, "")
|
||||
resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter)
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{
|
||||
{
|
||||
PresenterConfig: scanResultsPatched,
|
||||
Image: patchedImageName,
|
||||
},
|
||||
}
|
||||
resultsHandler.HandleResults(ctx)
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{*scanResultsPatched}
|
||||
|
||||
return scanResultsPatched, resultsHandler.HandleResults(ctx)
|
||||
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), scanResultsPatched.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
|
||||
}
|
||||
|
||||
func disableCopaLogger() {
|
||||
@@ -107,3 +138,252 @@ func disableCopaLogger() {
|
||||
null, _ := os.Open(os.DevNull)
|
||||
log.SetOutput(null)
|
||||
}
|
||||
|
||||
// copaPatch is a slightly modified copy of the Patch function from the original "project-copacetic/copacetic" repo
|
||||
// https://github.com/project-copacetic/copacetic/blob/main/pkg/patch/patch.go
|
||||
func copaPatch(ctx context.Context, timeout time.Duration, buildkitAddr, image, reportFile, patchedImageName, workingFolder string, ignoreError bool, bkOpts buildkit.Opts) error {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
ch <- patchWithContext(timeoutCtx, buildkitAddr, image, reportFile, patchedImageName, workingFolder, ignoreError, bkOpts)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-timeoutCtx.Done():
|
||||
// add a grace period for long running deferred cleanup functions to complete
|
||||
<-time.After(1 * time.Second)
|
||||
|
||||
err := fmt.Errorf("patch exceeded timeout %v", timeout)
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patchedImageName, workingFolder string, ignoreError bool, bkOpts buildkit.Opts) error {
|
||||
// Ensure working folder exists for call to InstallUpdates
|
||||
if workingFolder == "" {
|
||||
var err error
|
||||
workingFolder, err = os.MkdirTemp("", "copa-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(workingFolder)
|
||||
if err := os.Chmod(workingFolder, 0o744); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if isNew, err := utils.EnsurePath(workingFolder, 0o744); err != nil {
|
||||
log.Errorf("failed to create workingFolder %s", workingFolder)
|
||||
return err
|
||||
} else if isNew {
|
||||
defer os.RemoveAll(workingFolder)
|
||||
}
|
||||
}
|
||||
|
||||
var updates *unversioned.UpdateManifest
|
||||
// Parse report for update packages
|
||||
updates, err := tryParseScanReport(reportFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bkClient, err := buildkit.NewClient(ctx, bkOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copa: error creating buildkit client :: %w", err)
|
||||
}
|
||||
defer bkClient.Close()
|
||||
|
||||
dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
|
||||
cfg := authprovider.DockerAuthProviderConfig{ConfigFile: dockerConfig}
|
||||
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(cfg)}
|
||||
solveOpt := client.SolveOpt{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: client.ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": patchedImageName,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Frontend: "", // i.e. we are passing in the llb.Definition directly
|
||||
Session: attachable, // used for authprovider, sshagentprovider and secretprovider
|
||||
}
|
||||
solveOpt.SourcePolicy, err = build.ReadSourcePolicy()
|
||||
if err != nil {
|
||||
return fmt.Errorf("copa: error reading source policy :: %w", err)
|
||||
}
|
||||
|
||||
buildChannel := make(chan *client.SolveStatus)
|
||||
_, err = bkClient.Build(ctx, solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) {
|
||||
// Configure buildctl/client for use by package manager
|
||||
config, err := buildkit.InitializeBuildkitConfig(ctx, c, image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copa: error initializing buildkit config for image %s :: %w", image, err)
|
||||
}
|
||||
|
||||
// Create package manager helper
|
||||
var manager pkgmgr.PackageManager
|
||||
if reportFile == "" {
|
||||
// determine OS family
|
||||
fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract /etc/os-release file from state %w", err)
|
||||
}
|
||||
|
||||
osType, err := getOSType(ctx, fileBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copa: error getting os type :: %w", err)
|
||||
}
|
||||
|
||||
osVersion, err := getOSVersion(ctx, fileBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copa: error getting os version :: %w", err)
|
||||
}
|
||||
|
||||
// get package manager based on os family type
|
||||
manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copa: error getting package manager for ostype=%s, version=%s :: %w", osType, osVersion, err)
|
||||
}
|
||||
// do not specify updates, will update all
|
||||
updates = nil
|
||||
} else {
|
||||
// get package manager based on os family type
|
||||
manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copa: error getting package manager by family type: ostype=%s, osversion=%s :: %w", updates.Metadata.OS.Type, updates.Metadata.OS.Version, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Export the patched image state to Docker
|
||||
// TODO: Add support for other output modes as buildctl does.
|
||||
log.Infof("Patching %d vulnerabilities", len(updates.Updates))
|
||||
patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError)
|
||||
log.Infof("Error is: %v", err)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
platform := platforms.Normalize(platforms.DefaultSpec())
|
||||
if platform.OS != "linux" {
|
||||
platform.OS = "linux"
|
||||
}
|
||||
|
||||
def, err := patchedImageState.Marshal(ctx, llb.Platform(platform))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.Solve(ctx, gwclient.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
Evaluate: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData)
|
||||
|
||||
// Currently can only validate updates if updating via scanner
|
||||
if reportFile != "" {
|
||||
// create a new manifest with the successfully patched packages
|
||||
validatedManifest := &unversioned.UpdateManifest{
|
||||
Metadata: unversioned.Metadata{
|
||||
OS: unversioned.OS{
|
||||
Type: updates.Metadata.OS.Type,
|
||||
Version: updates.Metadata.OS.Version,
|
||||
},
|
||||
Config: unversioned.Config{
|
||||
Arch: updates.Metadata.Config.Arch,
|
||||
},
|
||||
},
|
||||
Updates: []unversioned.UpdatePackage{},
|
||||
}
|
||||
for _, update := range updates.Updates {
|
||||
if !slices.Contains(errPkgs, update.Name) {
|
||||
validatedManifest.Updates = append(validatedManifest.Updates, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}, buildChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getOSType(ctx context.Context, osreleaseBytes []byte) (string, error) {
|
||||
r := bytes.NewReader(osreleaseBytes)
|
||||
osData, err := osrelease.Parse(ctx, r)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse os-release data %w", err)
|
||||
}
|
||||
|
||||
osType := strings.ToLower(osData["NAME"])
|
||||
switch {
|
||||
case strings.Contains(osType, "alpine"):
|
||||
return "alpine", nil
|
||||
case strings.Contains(osType, "debian"):
|
||||
return "debian", nil
|
||||
case strings.Contains(osType, "ubuntu"):
|
||||
return "ubuntu", nil
|
||||
case strings.Contains(osType, "amazon"):
|
||||
return "amazon", nil
|
||||
case strings.Contains(osType, "centos"):
|
||||
return "centos", nil
|
||||
case strings.Contains(osType, "mariner"):
|
||||
return "cbl-mariner", nil
|
||||
case strings.Contains(osType, "azure linux"):
|
||||
return "azurelinux", nil
|
||||
case strings.Contains(osType, "red hat"):
|
||||
return "redhat", nil
|
||||
case strings.Contains(osType, "rocky"):
|
||||
return "rocky", nil
|
||||
case strings.Contains(osType, "oracle"):
|
||||
return "oracle", nil
|
||||
case strings.Contains(osType, "alma"):
|
||||
return "alma", nil
|
||||
default:
|
||||
log.Error("unsupported osType ", osType)
|
||||
return "", errors.ErrUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func getOSVersion(ctx context.Context, osreleaseBytes []byte) (string, error) {
|
||||
r := bytes.NewReader(osreleaseBytes)
|
||||
osData, err := osrelease.Parse(ctx, r)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse os-release data %w", err)
|
||||
}
|
||||
|
||||
return osData["VERSION_ID"], nil
|
||||
}
|
||||
|
||||
// This function adds support to copa for patching Kubescape produced results
|
||||
func tryParseScanReport(file string) (*unversioned.UpdateManifest, error) {
|
||||
|
||||
parser := copaGrype.GrypeParser{}
|
||||
manifest, err := parser.Parse(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert from v1alpha1 to unversioned.UpdateManifest
|
||||
var um unversioned.UpdateManifest
|
||||
um.Metadata.OS.Type = manifest.Metadata.OS.Type
|
||||
um.Metadata.OS.Version = manifest.Metadata.OS.Version
|
||||
um.Metadata.Config.Arch = manifest.Metadata.Config.Arch
|
||||
um.Updates = make([]unversioned.UpdatePackage, len(manifest.Updates))
|
||||
for i, update := range manifest.Updates {
|
||||
um.Updates[i].Name = update.Name
|
||||
um.Updates[i].InstalledVersion = update.InstalledVersion
|
||||
um.Updates[i].FixedVersion = update.FixedVersion
|
||||
um.Updates[i].VulnerabilityID = update.VulnerabilityID
|
||||
}
|
||||
|
||||
return &um, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/kubescape/backend/pkg/versioncheck"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/k8sinterface"
|
||||
@@ -20,10 +22,9 @@ import (
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter"
|
||||
"github.com/kubescape/kubescape/v3/pkg/imagescan"
|
||||
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
|
||||
"go.opentelemetry.io/otel"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/kubescape/opa-utils/resources"
|
||||
"go.opentelemetry.io/otel"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type componentInterfaces struct {
|
||||
@@ -41,10 +42,13 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt
|
||||
|
||||
// ================== setup k8s interface object ======================================
|
||||
var k8s *k8sinterface.KubernetesApi
|
||||
var k8sClient kubernetes.Interface
|
||||
if scanInfo.GetScanningContext() == cautils.ContextCluster {
|
||||
k8s = getKubernetesApi()
|
||||
if k8s == nil {
|
||||
logger.L().Ctx(ctx).Fatal("failed connecting to Kubernetes cluster")
|
||||
} else {
|
||||
k8sClient = k8s.KubernetesClient
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +66,11 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt
|
||||
}
|
||||
|
||||
// ================== version testing ======================================
|
||||
|
||||
v := cautils.NewIVersionCheckHandler(ctx)
|
||||
v.CheckLatestVersion(ctx, cautils.NewVersionCheckRequest(cautils.BuildNumber, policyIdentifierIdentities(scanInfo.PolicyIdentifier), "", cautils.ScanningContextToScanningScope(scanInfo.GetScanningContext())))
|
||||
// Skip version check in air-gapped mode (when keep-local flag is set)
|
||||
if !scanInfo.Local {
|
||||
v := versioncheck.NewIVersionCheckHandler(ctx)
|
||||
_ = v.CheckLatestVersion(ctx, versioncheck.NewVersionCheckRequest(scanInfo.AccountID, versioncheck.BuildNumber, policyIdentifierIdentities(scanInfo.PolicyIdentifier), "", string(scanInfo.GetScanningContext()), k8sClient))
|
||||
}
|
||||
|
||||
// ================== setup host scanner object ======================================
|
||||
ctxHostScanner, spanHostScanner := otel.Tracer("").Start(ctx, "setup host scanner")
|
||||
@@ -117,17 +123,26 @@ func GetOutputPrinters(scanInfo *cautils.ScanInfo, ctx context.Context, clusterN
|
||||
return outputPrinters
|
||||
}
|
||||
|
||||
func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
|
||||
ctxInit, spanInit := otel.Tracer("").Start(ctx, "initialization")
|
||||
func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
|
||||
ctxInit, spanInit := otel.Tracer("").Start(ks.Context(), "initialization")
|
||||
logger.L().Start("Kubescape scanner initializing...")
|
||||
|
||||
// ===================== Initialization =====================
|
||||
scanInfo.Init(ctxInit) // initialize scan info
|
||||
defer scanInfo.Cleanup()
|
||||
|
||||
interfaces := getInterfaces(ctxInit, scanInfo)
|
||||
interfaces.report.SetTenantConfig(interfaces.tenantConfig)
|
||||
|
||||
downloadReleasedPolicy := getter.NewDownloadReleasedPolicy() // download config inputs from github release
|
||||
// Only create DownloadReleasedPolicy if not in air-gapped mode
|
||||
var downloadReleasedPolicy *getter.DownloadReleasedPolicy
|
||||
if isAirGappedMode(scanInfo) {
|
||||
// In air-gapped mode (--keep-local or using local files via --use-from, --controls-config, --exceptions, or attack tracks),
|
||||
// don't initialize the downloader to prevent network access
|
||||
downloadReleasedPolicy = nil
|
||||
} else {
|
||||
downloadReleasedPolicy = getter.NewDownloadReleasedPolicy() // download config inputs from github release
|
||||
}
|
||||
|
||||
// set policy getter only after setting the customerGUID
|
||||
scanInfo.Getters.PolicyGetter = getPolicyGetter(ctxInit, scanInfo.UseFrom, interfaces.tenantConfig.GetAccountID(), scanInfo.FrameworkScan, downloadReleasedPolicy)
|
||||
@@ -143,7 +158,7 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res
|
||||
// remove host scanner components
|
||||
defer func() {
|
||||
if err := interfaces.hostSensorHandler.TearDown(); err != nil {
|
||||
logger.L().Ctx(ctx).StopError("Failed to tear down host scanner", helpers.Error(err))
|
||||
logger.L().Ctx(ks.Context()).StopError("Failed to tear down host scanner", helpers.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -163,7 +178,7 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res
|
||||
|
||||
// ===================== resources =====================
|
||||
ctxResources, spanResources := otel.Tracer("").Start(ctxInit, "resources")
|
||||
err = resourcehandler.CollectResources(ctxResources, interfaces.resourceHandler, scanInfo.PolicyIdentifier, scanData, cautils.NewProgressHandler(""), scanInfo)
|
||||
err = resourcehandler.CollectResources(ctxResources, interfaces.resourceHandler, scanData, scanInfo)
|
||||
if err != nil {
|
||||
spanInit.End()
|
||||
return resultsHandling, err
|
||||
@@ -172,11 +187,11 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res
|
||||
spanInit.End()
|
||||
|
||||
// ========================= opa testing =====================
|
||||
ctxOpa, spanOpa := otel.Tracer("").Start(ctx, "opa testing")
|
||||
ctxOpa, spanOpa := otel.Tracer("").Start(ks.Context(), "opa testing")
|
||||
defer spanOpa.End()
|
||||
|
||||
deps := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig(), interfaces.tenantConfig.GetContextName())
|
||||
reportResults := opaprocessor.NewOPAProcessor(scanData, deps, interfaces.tenantConfig.GetContextName())
|
||||
reportResults := opaprocessor.NewOPAProcessor(scanData, deps, interfaces.tenantConfig.GetContextName(), scanInfo.ExcludedNamespaces, scanInfo.IncludeNamespaces, scanInfo.EnableRegoPrint)
|
||||
if err = reportResults.ProcessRulesListener(ctxOpa, cautils.NewProgressHandler("")); err != nil {
|
||||
// TODO - do something
|
||||
return resultsHandling, fmt.Errorf("%w", err)
|
||||
@@ -186,18 +201,18 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res
|
||||
if scanInfo.PrintAttackTree || isPrioritizationScanType(scanInfo.ScanType) {
|
||||
_, spanPrioritization := otel.Tracer("").Start(ctxOpa, "prioritization")
|
||||
if priotizationHandler, err := resourcesprioritization.NewResourcesPrioritizationHandler(ctxOpa, scanInfo.Getters.AttackTracksGetter, scanInfo.PrintAttackTree); err != nil {
|
||||
logger.L().Ctx(ctx).Warning("failed to get attack tracks, this may affect the scanning results", helpers.Error(err))
|
||||
logger.L().Ctx(ks.Context()).Warning("failed to get attack tracks, this may affect the scanning results", helpers.Error(err))
|
||||
} else if err := priotizationHandler.PrioritizeResources(scanData); err != nil {
|
||||
return resultsHandling, fmt.Errorf("%w", err)
|
||||
}
|
||||
if err == nil && isPrioritizationScanType(scanInfo.ScanType) {
|
||||
if isPrioritizationScanType(scanInfo.ScanType) {
|
||||
scanData.SetTopWorkloads()
|
||||
}
|
||||
spanPrioritization.End()
|
||||
}
|
||||
|
||||
if scanInfo.ScanImages {
|
||||
scanImages(scanInfo.ScanType, scanData, ctx, resultsHandling)
|
||||
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling, scanInfo)
|
||||
}
|
||||
// ========================= results handling =====================
|
||||
resultsHandling.SetData(scanData)
|
||||
@@ -209,8 +224,8 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res
|
||||
return resultsHandling, nil
|
||||
}
|
||||
|
||||
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler) {
|
||||
imagesToScan := []string{}
|
||||
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler, scanInfo *cautils.ScanInfo) {
|
||||
var imagesToScan []string
|
||||
|
||||
if scanType == cautils.ScanTypeWorkload {
|
||||
containers, err := workloadinterface.NewWorkloadObj(scanData.SingleResourceScan.GetObject()).GetContainers()
|
||||
@@ -238,8 +253,13 @@ func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx
|
||||
}
|
||||
}
|
||||
|
||||
dbCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc := imagescan.NewScanService(dbCfg)
|
||||
distCfg, installCfg, _ := imagescan.NewDefaultDBConfig()
|
||||
svc, err := imagescan.NewScanServiceWithMatchers(distCfg, installCfg, scanInfo.UseDefaultMatchers)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
|
||||
return
|
||||
}
|
||||
defer svc.Close()
|
||||
|
||||
for _, img := range imagesToScan {
|
||||
logger.L().Start("Scanning", helpers.String("image", img))
|
||||
@@ -250,20 +270,27 @@ func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx
|
||||
}
|
||||
}
|
||||
|
||||
func scanSingleImage(ctx context.Context, img string, svc imagescan.Service, resultsHandling *resultshandling.ResultsHandler) error {
|
||||
func scanSingleImage(ctx context.Context, img string, svc *imagescan.Service, resultsHandling *resultshandling.ResultsHandler) error {
|
||||
|
||||
scanResults, err := svc.Scan(ctx, img, imagescan.RegistryCredentials{})
|
||||
scanResults, err := svc.Scan(ctx, img, imagescan.RegistryCredentials{}, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resultsHandling.ImageScanData = append(resultsHandling.ImageScanData, cautils.ImageScanData{
|
||||
Image: img,
|
||||
PresenterConfig: scanResults,
|
||||
})
|
||||
resultsHandling.ImageScanData = append(resultsHandling.ImageScanData, *scanResults)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPrioritizationScanType(scanType cautils.ScanTypes) bool {
|
||||
return scanType == cautils.ScanTypeCluster || scanType == cautils.ScanTypeRepo
|
||||
}
|
||||
|
||||
// isAirGappedMode returns true if the scan is configured to run in air-gapped mode
|
||||
// (i.e., without any network access to download policies, exceptions, or other artifacts)
|
||||
func isAirGappedMode(scanInfo *cautils.ScanInfo) bool {
|
||||
return scanInfo.Local ||
|
||||
len(scanInfo.UseFrom) > 0 ||
|
||||
scanInfo.ControlsInputs != "" ||
|
||||
scanInfo.UseExceptions != "" ||
|
||||
scanInfo.AttackTracks != ""
|
||||
}
|
||||
|
||||
@@ -58,3 +58,66 @@ func TestIsPrioritizationScanType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAirGappedMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scanInfo *cautils.ScanInfo
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "air-gapped with Local flag",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
Local: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "air-gapped with UseFrom",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
UseFrom: []string{"/path/to/policy"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "air-gapped with ControlsInputs",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
ControlsInputs: "/path/to/controls",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "air-gapped with UseExceptions",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
UseExceptions: "/path/to/exceptions",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "air-gapped with AttackTracks",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
AttackTracks: "/path/to/attack-tracks",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not air-gapped - all empty",
|
||||
scanInfo: &cautils.ScanInfo{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "air-gapped with multiple flags",
|
||||
scanInfo: &cautils.ScanInfo{
|
||||
Local: true,
|
||||
UseFrom: []string{"/path/to/policy"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, isAirGappedMode(tt.scanInfo))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user