mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-15 10:30:02 +00:00
Compare commits
240 Commits
pool
...
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 |
189
.github/workflows/00-pr-scanner.yaml
vendored
189
.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,19 +23,20 @@ jobs:
|
||||
pr-scanner:
|
||||
permissions:
|
||||
actions: read
|
||||
attestations: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
discussions: read
|
||||
models: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: write
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
attestations: read
|
||||
contents: write
|
||||
uses: ./.github/workflows/a-pr-scanner.yaml
|
||||
with:
|
||||
RELEASE: ""
|
||||
@@ -43,29 +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: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
attestations: read
|
||||
uses: ./.github/workflows/b-binary-build-and-e2e-tests.yaml
|
||||
with:
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: ""
|
||||
GO_VERSION: "1.23"
|
||||
RELEASE: "latest"
|
||||
CLIENT: test
|
||||
secrets: inherit
|
||||
contents: read
|
||||
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}}
|
||||
|
||||
213
.github/workflows/02-release.yaml
vendored
213
.github/workflows/02-release.yaml
vendored
@@ -3,110 +3,117 @@ 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: ubuntu22-core4-mem16-ssd150
|
||||
release:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
models: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
statuses: read
|
||||
security-events: read
|
||||
attestations: read
|
||||
artifact-metadata: read
|
||||
runs-on: ubuntu-large
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: tag-calculator
|
||||
uses: ./.github/actions/tag-action
|
||||
with:
|
||||
SUB_STRING: "-rc"
|
||||
binary-build:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: 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
|
||||
contents: write
|
||||
attestations: write
|
||||
needs: [retag]
|
||||
uses: ./.github/workflows/b-binary-build-and-e2e-tests.yaml
|
||||
with:
|
||||
COMPONENT_NAME: kubescape
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: ""
|
||||
GO_VERSION: "1.23"
|
||||
RELEASE: ${{ needs.retag.outputs.NEW_TAG }}
|
||||
CLIENT: release
|
||||
secrets: inherit
|
||||
create-release:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
statuses: read
|
||||
security-events: read
|
||||
attestations: 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
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: write
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
security-events: read
|
||||
statuses: read
|
||||
attestations: read
|
||||
contents: write
|
||||
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
|
||||
post-release:
|
||||
permissions:
|
||||
actions: read
|
||||
checks: 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
|
||||
attestations: read
|
||||
contents: write
|
||||
uses: ./.github/workflows/e-post-release.yaml
|
||||
needs: [publish-image]
|
||||
with:
|
||||
TAG: ${{ needs.retag.outputs.NEW_TAG }}
|
||||
secrets: inherit
|
||||
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
|
||||
|
||||
88
.github/workflows/a-pr-scanner.yaml
vendored
88
.github/workflows/a-pr-scanner.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
name: Create cross-platform build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: ubuntu22-core4-mem16-ssd150
|
||||
runs-on: ubuntu-large
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
@@ -48,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 }}
|
||||
@@ -66,86 +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@v3
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
version: v2.1
|
||||
args: --timeout 10m
|
||||
only-new-issues: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
|
||||
scanners:
|
||||
env:
|
||||
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
name: PR Scanner
|
||||
runs-on: ubuntu22-core4-mem16-ssd150
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- uses: actions/setup-go@v4
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: "1.23"
|
||||
- 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@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@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@v4
|
||||
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'
|
||||
|
||||
359
.github/workflows/b-binary-build-and-e2e-tests.yaml
vendored
359
.github/workflows/b-binary-build-and-e2e-tests.yaml
vendored
@@ -1,359 +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.23"
|
||||
GO111MODULE:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
CGO_ENABLED:
|
||||
type: number
|
||||
default: 1
|
||||
required: false
|
||||
BINARY_TESTS:
|
||||
type: string
|
||||
required: false
|
||||
default: '[
|
||||
"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_nsa",
|
||||
"scan_mitre",
|
||||
"scan_with_exceptions",
|
||||
"scan_repository",
|
||||
"scan_local_file",
|
||||
"scan_local_glob_files",
|
||||
"scan_local_list_of_files",
|
||||
"scan_with_exception_to_backend",
|
||||
"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_custom_framework",
|
||||
"scan_customer_configuration",
|
||||
"scan_compliance_score"
|
||||
]'
|
||||
|
||||
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.23"
|
||||
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_custom_framework",
|
||||
"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"
|
||||
]'
|
||||
|
||||
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 != '' && 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: ${{ 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-large
|
||||
steps:
|
||||
- name: (debug) Step 1 - Check disk space before checkout
|
||||
run: df -h
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: (debug) Step 2 - Check disk space before installing Go
|
||||
run: df -h
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
name: Installing go
|
||||
with:
|
||||
go-version: ${{ inputs.GO_VERSION }}
|
||||
|
||||
- name: (debug) Step 3 - Check disk space before build
|
||||
run: df -h
|
||||
|
||||
- name: Test core pkg
|
||||
run: ${{ env.DOCKER_CMD }} go test -v ./...
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
|
||||
- name: (debug) Step 4 - Check disk space before testing httphandler pkg
|
||||
run: df -h
|
||||
|
||||
- name: Test httphandler pkg
|
||||
run: ${{ env.DOCKER_CMD }} sh -c 'cd httphandler && go test -v ./...'
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
|
||||
- name: (debug) Step 5 - Check disk space before setting up Syft
|
||||
run: df -h
|
||||
|
||||
- uses: anchore/sbom-action/download-syft@v0
|
||||
name: Setup Syft
|
||||
|
||||
- name: (debug) Step 6 - Check disk space before goreleaser
|
||||
run: df -h
|
||||
|
||||
- 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: (debug) Step 7 - Check disk space before smoke testing
|
||||
run: df -h
|
||||
|
||||
- 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: (debug) Step 8 - Check disk space before golangci-lint
|
||||
run: df -h
|
||||
|
||||
- name: golangci-lint
|
||||
continue-on-error: true
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 10m
|
||||
only-new-issues: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
|
||||
- name: (debug) Step 9 - Check disk space before uploading artifacts
|
||||
run: df -h
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload artifacts
|
||||
with:
|
||||
name: kubescape
|
||||
path: dist/kubescape*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: (debug) Step 10 - Check disk space after uploading artifacts
|
||||
run: df -h
|
||||
|
||||
build-http-image:
|
||||
permissions:
|
||||
contents: write
|
||||
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.23"
|
||||
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_nsa",
|
||||
"scan_mitre",
|
||||
"scan_with_exceptions",
|
||||
"scan_repository",
|
||||
"scan_local_file",
|
||||
"scan_local_glob_files",
|
||||
"scan_local_list_of_files",
|
||||
"scan_with_exception_to_backend",
|
||||
"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_custom_framework",
|
||||
"scan_customer_configuration",
|
||||
"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@v4
|
||||
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@v4
|
||||
with:
|
||||
repository: armosec/system-tests
|
||||
path: .
|
||||
|
||||
- uses: 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@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 }}
|
||||
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@v5
|
||||
if: always() # always run even if the previous step fails
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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: write
|
||||
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.23"
|
||||
REQUIRED_TESTS: '[]'
|
||||
COSIGN: ${{ inputs.CO_SIGN }}
|
||||
HELM_E2E_TEST: false
|
||||
FORCE: true
|
||||
secrets: inherit
|
||||
92
.github/workflows/c-create-release.yaml
vendored
92
.github/workflows/c-create-release.yaml
vendored
@@ -1,92 +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@v4
|
||||
id: download-artifact
|
||||
with:
|
||||
name: kubescape
|
||||
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-${{ env.WINDOWS_OS }}.exe ${{steps.download-artifact.outputs.download-path}}/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@v2
|
||||
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-${{ env.MAC_OS }}
|
||||
./kubescape-${{ env.MAC_OS }}.sbom
|
||||
./kubescape-${{ env.MAC_OS }}.sha256
|
||||
./kubescape-${{ env.MAC_OS }}.tar.gz
|
||||
./kubescape-${{ env.UBUNTU_OS }}
|
||||
./kubescape-${{ env.UBUNTU_OS }}.sbom
|
||||
./kubescape-${{ env.UBUNTU_OS }}.sha256
|
||||
./kubescape-${{ env.UBUNTU_OS }}.tar.gz
|
||||
./kubescape-${{ env.WINDOWS_OS }}.exe
|
||||
./kubescape-${{ env.WINDOWS_OS }}.exe.sbom
|
||||
./kubescape-${{ env.WINDOWS_OS }}.exe.sha256
|
||||
./kubescape-${{ env.WINDOWS_OS }}.tar.gz
|
||||
./kubescape-arm64-${{ env.MAC_OS }}
|
||||
./kubescape-arm64-${{ env.MAC_OS }}.sbom
|
||||
./kubescape-arm64-${{ env.MAC_OS }}.sha256
|
||||
./kubescape-arm64-${{ env.MAC_OS }}.tar.gz
|
||||
./kubescape-arm64-${{ env.UBUNTU_OS }}
|
||||
./kubescape-arm64-${{ env.UBUNTU_OS }}.sbom
|
||||
./kubescape-arm64-${{ env.UBUNTU_OS }}.sha256
|
||||
./kubescape-arm64-${{ env.UBUNTU_OS }}.tar.gz
|
||||
./kubescape-arm64-${{ env.WINDOWS_OS }}.exe
|
||||
./kubescape-arm64-${{ env.WINDOWS_OS }}.exe.sbom
|
||||
./kubescape-arm64-${{ env.WINDOWS_OS }}.exe.sha256
|
||||
./kubescape-arm64-${{ env.WINDOWS_OS }}.tar.gz
|
||||
./kubescape-riscv64-${{ env.UBUNTU_OS }}
|
||||
./kubescape-riscv64-${{ env.UBUNTU_OS }}.sbom
|
||||
./kubescape-riscv64-${{ env.UBUNTU_OS }}.sha256
|
||||
./kubescape-riscv64-${{ env.UBUNTU_OS }}.tar.gz
|
||||
./kubescape.exe
|
||||
107
.github/workflows/d-publish-image.yaml
vendored
107
.github/workflows/d-publish-image.yaml
vendored
@@ -1,107 +0,0 @@
|
||||
name: d-publish-image
|
||||
permissions:
|
||||
actions: read
|
||||
checks: read
|
||||
contents: write
|
||||
deployments: read
|
||||
discussions: read
|
||||
id-token: write
|
||||
issues: read
|
||||
packages: read
|
||||
pages: read
|
||||
pull-requests: read
|
||||
repository-projects: read
|
||||
statuses: read
|
||||
security-events: read
|
||||
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@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- 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
|
||||
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@v4
|
||||
id: download-artifact
|
||||
with:
|
||||
name: kubescape
|
||||
path: .
|
||||
- name: mv kubescape amd64 binary
|
||||
run: mv kubescape-ubuntu-latest kubescape-amd64-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 }}
|
||||
46
.github/workflows/e-post-release.yaml
vendored
46
.github/workflows/e-post-release.yaml
vendored
@@ -1,46 +0,0 @@
|
||||
name: e-post_release
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
TAG:
|
||||
description: 'Tag name'
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
post_release:
|
||||
name: Post release jobs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Update new version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@v0.0.47
|
||||
if: github.repository_owner == 'kubescape'
|
||||
env:
|
||||
GITHUB_REF: ${{ inputs.TAG }}
|
||||
- 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 }}
|
||||
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@v2.4.0
|
||||
uses: ossf/scorecard-action@v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: 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@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@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,51 +1,57 @@
|
||||
linters-settings:
|
||||
govet:
|
||||
shadow: 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
|
||||
- errcheck
|
||||
- dupl
|
||||
- gocritic
|
||||
- errcheck
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- gocritic
|
||||
- lll
|
||||
- nakedret
|
||||
- revive
|
||||
- stylecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
#- forbidigo # <- see later
|
||||
# should remain disabled
|
||||
- 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$
|
||||
|
||||
138
.goreleaser.yaml
138
.goreleaser.yaml
@@ -1,46 +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:
|
||||
- goos:
|
||||
- id: cli
|
||||
binary: kubescape
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- riscv64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X "github.com/kubescape/kubescape/v3/core/cautils.BuildNumber={{.Env.RELEASE}}"
|
||||
- -X "github.com/kubescape/kubescape/v3/core/cautils.Client={{.Env.CLIENT}}"
|
||||
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
|
||||
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: binary
|
||||
id: binaries
|
||||
name_template: >-
|
||||
{{ .Binary }}
|
||||
- format: tar.gz
|
||||
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
|
||||
@@ -50,11 +123,24 @@ changelog:
|
||||
- "^test:"
|
||||
|
||||
checksum:
|
||||
ids:
|
||||
- binaries
|
||||
split: true
|
||||
name_template: "checksums.sha256"
|
||||
|
||||
sboms:
|
||||
- artifacts: binary
|
||||
documents:
|
||||
- "{{ .Binary }}.sbom"
|
||||
|
||||
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
|
||||
471
README.md
471
README.md
@@ -8,6 +8,7 @@
|
||||
[](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)
|
||||
@@ -22,100 +23,478 @@
|
||||
|
||||
_Comprehensive Kubernetes Security from Development to Runtime_
|
||||
|
||||
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. It saves Kubernetes users and admins precious time, effort, and resources.
|
||||
|
||||
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.armosec.io/glossary/mitre-attck-framework/?utm_source=github&utm_medium=repository) and the [CIS Benchmark](https://www.armosec.io/blog/cis-kubernetes-benchmark-framework-scanning-tools-comparison/?utm_source=github&utm_medium=repository)).
|
||||
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 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/).
|
||||
|
||||
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape! 😀_
|
||||
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape!_
|
||||
|
||||
## Demo
|
||||
---
|
||||
|
||||
Kubescape has a command line tool that you can use to quickly get a report on the security posture of a Kubernetes cluster:
|
||||
## 📑 Table of Contents
|
||||
|
||||
<img src="docs/img/demo-v3.gif">
|
||||
- [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
|
||||
```
|
||||
|
||||
This script will automatically download the latest Kubescape CLI release and scan the Kubernetes cluster in your current kubectl context.
|
||||
> 💡 See [Installation](#-installation) for more options (Homebrew, Krew, Windows, etc.)
|
||||
|
||||
Learn more about:
|
||||
### 2. Run Your First Scan
|
||||
|
||||
* [Installing the Kubescape CLI](https://kubescape.io/docs/install-cli/)
|
||||
* [Running your first scan](https://kubescape.io/docs/scanning/)
|
||||
* [Accepting risk with exceptions](https://kubescape.io/docs/accepting-risk/)
|
||||
```sh
|
||||
# Scan your current cluster
|
||||
kubescape scan
|
||||
|
||||
_Did you know you can use Kubescape in all these places?_
|
||||
# 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>
|
||||
|
||||
### Continuous security monitoring with the Kubescape Operator
|
||||
**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
|
||||
|
||||
As well as a CLI, Kubescape provides an in-cluster mode, which is installed via a Helm chart. Kubescape in-cluster provides extensive features such as continuous scanning, image vulnerability scanning, runtime analysis, network policy generation, and more. [Learn more about the Kubescape operator](https://kubescape.io/docs/operator/).
|
||||
### Operator Mode (In-Cluster)
|
||||
|
||||
### Using Kubescape as a 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).
|
||||
For image scanning, it uses [Grype](https://github.com/anchore/grype).
|
||||
For image patching, it uses [Copacetic](https://github.com/project-copacetic/copacetic).
|
||||
For eBPF, it uses [Inspektor Gadget](https://github.com/inspektor-gadget)
|
||||
📖 **[Full Architecture Documentation →](docs/architecture.md)**
|
||||
|
||||
By default, CLI scan results are printed in a console-friendly manner, but they can be:
|
||||
---
|
||||
|
||||
* exported to JSON, junit XML or SARIF
|
||||
* rendered to HTML or PDF
|
||||
* submitted to a [cloud service](docs/providers.md)
|
||||
## ☸️ In-Cluster Operator
|
||||
|
||||
### In-cluster architecture
|
||||
The Kubescape operator provides continuous security monitoring in your cluster:
|
||||
|
||||

|
||||
```bash
|
||||
# Add the Kubescape Helm repository
|
||||
helm repo add kubescape https://kubescape.github.io/helm-charts/
|
||||
|
||||
## Community
|
||||
# Install the operator
|
||||
helm upgrade --install kubescape kubescape/kubescape-operator \
|
||||
--namespace kubescape \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
Kubescape is an open source project. We welcome your feedback and ideas for improvement. We are part of the CNCF community and are evolving Kubescape in sync with the security needs of Kubernetes users. To learn more about where Kubescape is heading, please check out our [ROADMAP](https://github.com/kubescape/project-governance/blob/main/ROADMAP.md).
|
||||
**Operator Features:**
|
||||
- 🔄 Continuous misconfiguration scanning
|
||||
- 🐳 Image vulnerability scanning for all workloads
|
||||
- 🔍 Runtime threat detection (eBPF-based)
|
||||
- 🌐 Network policy generation
|
||||
- 📈 Prometheus metrics integration
|
||||
|
||||
If you feel inspired to contribute to Kubescape, check out our [CONTRIBUTING](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md) file to learn how. You can find the issues we are working on (triage to development) on the [Kubescaping board](https://github.com/orgs/kubescape/projects/4/views/1)
|
||||
📖 **[Operator Installation Guide →](https://kubescape.io/docs/operator/)**
|
||||
|
||||
* Feel free to pick a task from the [board](https://github.com/orgs/kubescape/projects/4) or suggest a feature of your own.
|
||||
* Open an issue on the board. 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.
|
||||
---
|
||||
|
||||
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
## 🔌 Integrations
|
||||
|
||||
For more information about the Kubescape community, please visit [COMMUNITY](https://github.com/kubescape/project-governance/blob/main/COMMUNITY.md).
|
||||
### CI/CD
|
||||
|
||||
| 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/) |
|
||||
|
||||
We would like to take this opportunity to thank all our contibutors to date.
|
||||
### 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-2024, 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 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/refs/heads/main/other/cncf-member/incubating/color/cncf-incubating-color.svg" width="300" alt="CNCF Incubating Project">
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +1,12 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm 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 .
|
||||
RUN --mount=target=. \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
go run downloader/main.go
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
FROM gcr.io/distroless/static-debian13:nonroot
|
||||
|
||||
USER nonroot
|
||||
WORKDIR /home/nonroot/
|
||||
|
||||
COPY --from=builder /out/ksserver /usr/bin/ksserver
|
||||
COPY --from=builder /root/.kubescape /home/nonroot/.kubescape
|
||||
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/static-debian12: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"]
|
||||
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
%[1]s list controls
|
||||
|
||||
Control documentation:
|
||||
https://hub.armosec.io/docs/controls
|
||||
https://kubescape.io/docs/controls/
|
||||
`, cautils.ExecName())
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -73,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
|
||||
|
||||
@@ -6,13 +6,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -28,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]",
|
||||
@@ -49,12 +49,15 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := ks.Patch(&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())
|
||||
}
|
||||
|
||||
@@ -76,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import (
|
||||
)
|
||||
|
||||
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()
|
||||
clientSet, inCluster := common.BuildKubeClient(*kubeconfigPath)
|
||||
if clientSet == nil {
|
||||
logger.L().Fatal("Could not create kube client. Exiting.")
|
||||
}
|
||||
@@ -42,5 +44,8 @@ func GetPreReqCmd(ks meta.IKubescape) *cobra.Command {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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"
|
||||
@@ -52,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)
|
||||
@@ -100,6 +101,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
|
||||
rootCmd.AddCommand(vap.GetVapHelperCmd())
|
||||
rootCmd.AddCommand(operator.GetOperatorCmd(ks))
|
||||
rootCmd.AddCommand(prerequisites.GetPreReqCmd(ks))
|
||||
rootCmd.AddCommand(mcpserver.GetMCPServerCmd())
|
||||
|
||||
// deprecated commands
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -29,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())
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman
|
||||
if err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
if err := results.HandleResults(ks.Context()); err != nil {
|
||||
if err := results.HandleResults(ks.Context(), scanInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
if !scanInfo.VerboseMode {
|
||||
|
||||
@@ -117,7 +117,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ks.Context()); err != nil {
|
||||
if err = results.HandleResults(ks.Context(), scanInfo); err != nil {
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -33,6 +32,7 @@ var (
|
||||
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]",
|
||||
@@ -54,18 +54,19 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
|
||||
}
|
||||
|
||||
imgScanInfo := &metav1.ImageScanInfo{
|
||||
Image: args[0],
|
||||
Username: imgCredentials.Username,
|
||||
Password: imgCredentials.Password,
|
||||
Exceptions: exceptions,
|
||||
Image: args[0],
|
||||
Username: imgCredentials.Username,
|
||||
Password: imgCredentials.Password,
|
||||
Exceptions: exceptions,
|
||||
UseDefaultMatchers: useDefaultMatchers,
|
||||
}
|
||||
|
||||
results, err := ks.ScanImage(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())
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
|
||||
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
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
|
||||
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.")
|
||||
@@ -139,7 +141,7 @@ func securityScan(scanInfo cautils.ScanInfo, ks meta.IKubescape) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ks.Context()); err != nil {
|
||||
if err = results.HandleResults(ks.Context(), &scanInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/cmd/shared"
|
||||
@@ -186,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]
|
||||
|
||||
@@ -70,7 +70,7 @@ func getWorkloadCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comma
|
||||
logger.L().Fatal(err.Error())
|
||||
}
|
||||
|
||||
if err = results.HandleResults(ks.Context()); 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]
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -220,9 +220,11 @@ func createPolicyBinding(bindingName string, policyName string, action string, p
|
||||
}
|
||||
|
||||
policyBinding.Spec.ValidationActions = []admissionv1.ValidationAction{admissionv1.ValidationAction(action)}
|
||||
paramAction := admissionv1.DenyAction
|
||||
if paramRefName != "" {
|
||||
policyBinding.Spec.ParamRef = &admissionv1.ParamRef{
|
||||
Name: paramRefName,
|
||||
Name: paramRefName,
|
||||
ParameterNotFoundAction: ¶mAction,
|
||||
}
|
||||
}
|
||||
// Marshal the policy binding to YAML
|
||||
|
||||
@@ -16,14 +16,11 @@ func GetVersionCmd(ks meta.IKubescape) *cobra.Command {
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := versioncheck.NewIVersionCheckHandler(ks.Context())
|
||||
versionCheckRequest := versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "version", nil)
|
||||
if err := v.CheckLatestVersion(ks.Context(), versionCheckRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.CheckLatestVersion(ks.Context(), versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "version", nil))
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStdout(),
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
|
||||
"Your current version is: %s\n",
|
||||
versionCheckRequest.ClientVersion,
|
||||
versioncheck.BuildNumber,
|
||||
)
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestGetVersionCmd(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Undefined Build Number",
|
||||
buildNumber: "",
|
||||
buildNumber: "unknown",
|
||||
want: "Your current version is: unknown\n",
|
||||
},
|
||||
{
|
||||
|
||||
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)
|
||||
@@ -24,8 +24,7 @@ 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
|
||||
@@ -207,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()
|
||||
|
||||
@@ -270,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 {
|
||||
@@ -291,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
|
||||
}
|
||||
|
||||
@@ -397,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)
|
||||
}
|
||||
@@ -413,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
|
||||
@@ -60,6 +69,7 @@ type OPASessionObj struct {
|
||||
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 {
|
||||
@@ -78,6 +88,7 @@ func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework
|
||||
OmitRawResources: scanInfo.OmitRawResources,
|
||||
TriggeredByCLI: scanInfo.TriggeredByCLI,
|
||||
TemplateMapping: make(map[string]MappingNodes),
|
||||
LabelsToCopy: scanInfo.LabelsToCopy,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,7 +15,12 @@ import (
|
||||
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 {
|
||||
@@ -24,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
|
||||
@@ -36,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()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -75,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")
|
||||
}
|
||||
|
||||
@@ -137,8 +137,10 @@ type ScanInfo struct {
|
||||
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()
|
||||
}
|
||||
@@ -320,6 +322,9 @@ func (scanInfo *ScanInfo) getScanningContext(input string) ScanningContext {
|
||||
return ContextCluster
|
||||
}
|
||||
|
||||
// Check if input is a URL (http:// or https://)
|
||||
isURL := isHTTPURL(input)
|
||||
|
||||
// git url
|
||||
if _, err := giturl.NewGitURL(input); err == nil {
|
||||
if repo, err := CloneGitRepo(&input); err == nil {
|
||||
@@ -330,6 +335,18 @@ func (scanInfo *ScanInfo) getScanningContext(input string) ScanningContext {
|
||||
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
|
||||
@@ -455,3 +472,8 @@ func getAbsPath(p string) string {
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// 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://")
|
||||
}
|
||||
|
||||
@@ -88,6 +88,16 @@ func TestGetScanningContext(t *testing.T) {
|
||||
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) {
|
||||
|
||||
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
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
ksmetav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
|
||||
@@ -111,7 +110,9 @@ func regexStringMatch(pattern, target string) bool {
|
||||
// exception policy.
|
||||
func isTargetImage(targets []Target, attributes Attributes) bool {
|
||||
for _, target := range targets {
|
||||
return regexStringMatch(target.Attributes.Registry, attributes.Registry) && regexStringMatch(target.Attributes.Organization, attributes.Organization) && regexStringMatch(target.Attributes.ImageName, attributes.ImageName) && regexStringMatch(target.Attributes.ImageTag, attributes.ImageTag)
|
||||
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
|
||||
@@ -161,11 +162,16 @@ func getUniqueVulnerabilitiesAndSeverities(policies []VulnerabilitiesIgnorePolic
|
||||
return uniqueVulnsList, uniqueSeversList
|
||||
}
|
||||
|
||||
func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
|
||||
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,
|
||||
@@ -178,16 +184,16 @@ func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *ca
|
||||
exceptionPolicies, err := GetImageExceptionsFromFile(imgScanInfo.Exceptions)
|
||||
if err != nil {
|
||||
logger.L().StopError(fmt.Sprintf("Failed to load exceptions from file: %s", imgScanInfo.Exceptions))
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
vulnerabilityExceptions, severityExceptions = getUniqueVulnerabilitiesAndSeverities(exceptionPolicies, imgScanInfo.Image)
|
||||
}
|
||||
|
||||
scanResults, err := svc.Scan(ks.Context(), imgScanInfo.Image, creds, vulnerabilityExceptions, severityExceptions)
|
||||
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))
|
||||
@@ -200,12 +206,7 @@ func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *ca
|
||||
|
||||
resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter)
|
||||
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{
|
||||
{
|
||||
PresenterConfig: scanResults,
|
||||
Image: imgScanInfo.Image,
|
||||
},
|
||||
}
|
||||
resultsHandler.ImageScanData = []cautils.ImageScanData{*imageScanData}
|
||||
|
||||
return scanResults, resultsHandler.HandleResults(ks.Context())
|
||||
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), imageScanData.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
|
||||
}
|
||||
|
||||
@@ -241,6 +241,33 @@ func TestIsTargetImage(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
|
||||
@@ -90,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())
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
@@ -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,15 +1,22 @@
|
||||
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"
|
||||
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"
|
||||
@@ -17,21 +24,37 @@ import (
|
||||
"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/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(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,
|
||||
@@ -39,15 +62,21 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
|
||||
// Scan the image
|
||||
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 scanResults.ID.Name == "" {
|
||||
scanResults.ID.Name = "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, "/", "-")
|
||||
@@ -55,7 +84,7 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
|
||||
writer := printer.GetWriter(ks.Context(), fileName)
|
||||
|
||||
if err = pres.Present(writer); err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", patchInfo.Image))
|
||||
|
||||
@@ -69,7 +98,7 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
|
||||
}
|
||||
|
||||
if err = copaPatch(ks.Context(), patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchedImageName, "", patchInfo.IgnoreError, patchInfo.BuildKitOpts); err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Restore the output streams
|
||||
@@ -83,7 +112,7 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
|
||||
|
||||
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))
|
||||
|
||||
@@ -99,14 +128,9 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
|
||||
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.ImageScanData = []cautils.ImageScanData{*scanResultsPatched}
|
||||
|
||||
return scanResultsPatched, resultsHandler.HandleResults(ks.Context())
|
||||
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), scanResultsPatched.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
|
||||
}
|
||||
|
||||
func disableCopaLogger() {
|
||||
@@ -160,43 +184,185 @@ func patchWithContext(ctx context.Context, buildkitAddr, image, reportFile, patc
|
||||
}
|
||||
}
|
||||
|
||||
var updates *unversioned.UpdateManifest
|
||||
// Parse report for update packages
|
||||
updates, err := tryParseScanReport(reportFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := buildkit.NewClient(ctx, bkOpts)
|
||||
bkClient, err := buildkit.NewClient(ctx, bkOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("copa: error creating buildkit client :: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer bkClient.Close()
|
||||
|
||||
// Configure buildctl/client for use by package manager
|
||||
config, err := buildkit.InitializeBuildkitConfig(ctx, client, image, updates)
|
||||
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 err
|
||||
return fmt.Errorf("copa: error reading source policy :: %w", err)
|
||||
}
|
||||
|
||||
// Create package manager helper
|
||||
pkgmgr, err := pkgmgr.GetPackageManager(updates.Metadata.OS.Type, config, workingFolder)
|
||||
if err != nil {
|
||||
return 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)
|
||||
}
|
||||
|
||||
// Export the patched image state to Docker
|
||||
patchedImageState, _, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError)
|
||||
if err != nil {
|
||||
return 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)
|
||||
}
|
||||
|
||||
if err = buildkit.SolveToDocker(ctx, config.Client, patchedImageState, config.ConfigData, patchedImageName); err != nil {
|
||||
return 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) {
|
||||
|
||||
|
||||
@@ -66,9 +66,11 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt
|
||||
}
|
||||
|
||||
// ================== version testing ======================================
|
||||
|
||||
v := versioncheck.NewIVersionCheckHandler(ctx)
|
||||
_ = v.CheckLatestVersion(ctx, versioncheck.NewVersionCheckRequest(scanInfo.AccountID, versioncheck.BuildNumber, policyIdentifierIdentities(scanInfo.PolicyIdentifier), "", string(scanInfo.GetScanningContext()), k8sClient))
|
||||
// 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")
|
||||
@@ -132,7 +134,15 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
|
||||
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)
|
||||
@@ -202,7 +212,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
|
||||
}
|
||||
|
||||
if scanInfo.ScanImages {
|
||||
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling)
|
||||
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling, scanInfo)
|
||||
}
|
||||
// ========================= results handling =====================
|
||||
resultsHandling.SetData(scanData)
|
||||
@@ -214,7 +224,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
|
||||
return resultsHandling, nil
|
||||
}
|
||||
|
||||
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler) {
|
||||
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler, scanInfo *cautils.ScanInfo) {
|
||||
var imagesToScan []string
|
||||
|
||||
if scanType == cautils.ScanTypeWorkload {
|
||||
@@ -243,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))
|
||||
@@ -255,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{}, 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package v1
|
||||
|
||||
type ImageScanInfo struct {
|
||||
Username string
|
||||
Password string
|
||||
Image string
|
||||
Exceptions string
|
||||
Username string
|
||||
Password string
|
||||
Image string
|
||||
Exceptions string
|
||||
UseDefaultMatchers bool
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package meta
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"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"
|
||||
@@ -27,8 +26,8 @@ type IKubescape interface {
|
||||
Fix(fixInfo *metav1.FixInfo) error
|
||||
|
||||
// patch
|
||||
Patch(patchInfo *metav1.PatchInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error)
|
||||
Patch(patchInfo *metav1.PatchInfo, scanInfo *cautils.ScanInfo) (bool, error)
|
||||
|
||||
// scan image
|
||||
ScanImage(imgScanInfo *metav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error)
|
||||
ScanImage(imgScanInfo *metav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (bool, error)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package mocks
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"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"
|
||||
@@ -15,38 +14,38 @@ func (m *MockIKubescape) Context() context.Context {
|
||||
return context.TODO()
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
|
||||
func (m *MockIKubescape) Scan(_ *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) List(listPolicies *metav1.ListPolicies) error {
|
||||
func (m *MockIKubescape) List(_ *metav1.ListPolicies) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) Download(downloadInfo *metav1.DownloadInfo) error {
|
||||
func (m *MockIKubescape) Download(_ *metav1.DownloadInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) SetCachedConfig(setConfig *metav1.SetConfig) error {
|
||||
func (m *MockIKubescape) SetCachedConfig(_ *metav1.SetConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) ViewCachedConfig(viewConfig *metav1.ViewConfig) error {
|
||||
func (m *MockIKubescape) ViewCachedConfig(_ *metav1.ViewConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) DeleteCachedConfig(deleteConfig *metav1.DeleteConfig) error {
|
||||
func (m *MockIKubescape) DeleteCachedConfig(_ *metav1.DeleteConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) Fix(fixInfo *metav1.FixInfo) error {
|
||||
func (m *MockIKubescape) Fix(_ *metav1.FixInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) Patch(patchInfo *metav1.PatchInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
|
||||
return nil, nil
|
||||
func (m *MockIKubescape) Patch(_ *metav1.PatchInfo, _ *cautils.ScanInfo) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *MockIKubescape) ScanImage(imgScanInfo *metav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
|
||||
return nil, nil
|
||||
func (m *MockIKubescape) ScanImage(_ *metav1.ImageScanInfo, _ *cautils.ScanInfo) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ spec:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: true
|
||||
readOnlyRootFilesystem: true
|
||||
procMount: Unmasked
|
||||
ports:
|
||||
- name: scanner # Do not change port name
|
||||
containerPort: 7888
|
||||
|
||||
@@ -96,7 +96,7 @@ func (hsh *HostSensorHandler) Init(ctx context.Context) error {
|
||||
|
||||
hsh.populatePodNamesToNodeNames(ctx, log)
|
||||
if err := hsh.checkPodForEachNode(); err != nil {
|
||||
logger.L().Ctx(ctx).Warning(failedToValidateHostSensorPodStatus, helpers.Error(err))
|
||||
return fmt.Errorf("%s: %v", failedToValidateHostSensorPodStatus, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes/hostsensor"
|
||||
)
|
||||
|
||||
@@ -28,19 +27,18 @@ type workerPool struct {
|
||||
|
||||
func newWorkerPool() workerPool {
|
||||
wp := workerPool{}
|
||||
wp.noOfWorkers, _ = cautils.ParseIntEnvVar("KUBESCAPE_WORKERS", noOfWorkers)
|
||||
wp.noOfWorkers = noOfWorkers
|
||||
wp.init()
|
||||
return wp
|
||||
}
|
||||
|
||||
func (wp *workerPool) init(noOfPods ...int) {
|
||||
if len(noOfPods) > 0 && noOfPods[0] < wp.noOfWorkers {
|
||||
if len(noOfPods) > 0 && noOfPods[0] < noOfWorkers {
|
||||
wp.noOfWorkers = noOfPods[0]
|
||||
}
|
||||
logger.L().Debug("Initializing worker pool", helpers.Int("noOfWorkers", wp.noOfWorkers))
|
||||
// init the channels
|
||||
wp.jobs = make(chan job, wp.noOfWorkers)
|
||||
wp.results = make(chan hostsensor.HostSensorDataEnvelope, wp.noOfWorkers)
|
||||
wp.jobs = make(chan job, noOfWorkers)
|
||||
wp.results = make(chan hostsensor.HostSensorDataEnvelope, noOfWorkers)
|
||||
wp.done = make(chan bool)
|
||||
}
|
||||
|
||||
@@ -59,7 +57,7 @@ func (wp *workerPool) hostSensorWorker(ctx context.Context, hsh *HostSensorHandl
|
||||
}
|
||||
|
||||
func (wp *workerPool) createWorkerPool(ctx context.Context, hsh *HostSensorHandler, wg *sync.WaitGroup, log *LogsMap) {
|
||||
for i := 0; i < wp.noOfWorkers; i++ {
|
||||
for i := 0; i < noOfWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go wp.hostSensorWorker(ctx, hsh, wg, log)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ spec:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: true
|
||||
readOnlyRootFilesystem: true
|
||||
procMount: Unmasked
|
||||
ports:
|
||||
- name: scanner # Do not change port name
|
||||
containerPort: 7888
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
"github.com/sigstore/cosign/v3/pkg/cosign"
|
||||
)
|
||||
|
||||
func has_signature(img string) bool {
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
|
||||
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
|
||||
sigs "github.com/sigstore/cosign/v2/pkg/signature"
|
||||
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v3/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/v3/pkg/cosign"
|
||||
"github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key"
|
||||
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
|
||||
sigs "github.com/sigstore/cosign/v3/pkg/signature"
|
||||
)
|
||||
|
||||
// VerifyCommand verifies a signature on a supplied container image
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/kubescape/opa-utils/resources"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
opaprint "github.com/open-policy-agent/opa/topdown/print"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
"github.com/open-policy-agent/opa/v1/storage"
|
||||
opaprint "github.com/open-policy-agent/opa/v1/topdown/print"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
@@ -324,6 +324,7 @@ func (opap *OPAProcessor) runRegoOnK8s(ctx context.Context, rule *reporthandling
|
||||
// NOTE: OPA module compilation is the most resource-intensive operation.
|
||||
compiled, err := ast.CompileModulesWithOpt(modules, ast.CompileOpts{
|
||||
EnablePrintStatements: opap.printEnabled,
|
||||
ParserOptions: ast.ParserOptions{RegoVersion: ast.RegoV0},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in 'runRegoOnK8s', failed to compile rule, name: %s, reason: %w", rule.Name, err)
|
||||
@@ -351,6 +352,7 @@ func (opap *OPAProcessor) Print(ctx opaprint.Context, str string) error {
|
||||
|
||||
func (opap *OPAProcessor) regoEval(ctx context.Context, inputObj []map[string]interface{}, compiledRego *ast.Compiler, store *storage.Store) ([]reporthandling.RuleResponse, error) {
|
||||
rego := rego.New(
|
||||
rego.SetRegoVersion(ast.RegoV0),
|
||||
rego.Query("data.armo_builtins"), // get package name from rule
|
||||
rego.Compiler(compiledRego),
|
||||
rego.Input(inputObj),
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
"github.com/open-policy-agent/opa/types"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
"github.com/open-policy-agent/opa/v1/topdown/builtins"
|
||||
"github.com/open-policy-agent/opa/v1/types"
|
||||
)
|
||||
|
||||
// convertFrameworksToPolicies convert list of frameworks to list of policies
|
||||
|
||||
@@ -232,7 +232,7 @@ func (k8sHandler *K8sResourceHandler) collectCloudResources(ctx context.Context,
|
||||
if !strings.Contains(err.Error(), cloudv1.NotSupportedMsg) {
|
||||
// Return error with useful info on how to configure credentials for getting cloud provider info
|
||||
logger.L().Debug("failed to get cloud data", helpers.String("resourceKind", resourceKind), helpers.Error(err))
|
||||
err = fmt.Errorf("failed to get %s descriptive information. Read more: https://hub.armosec.io/docs/kubescape-integration-with-cloud-providers", strings.ToUpper(k8sHandler.cloudProvider))
|
||||
err = fmt.Errorf("failed to get %s descriptive information. Read more: https://kubescape.io/docs/integrations/kubescape-integration-with-cloud-providers/", strings.ToUpper(k8sHandler.cloudProvider))
|
||||
cautils.SetInfoMapForResources(err.Error(), cloudResources, sessionObj.InfoMap)
|
||||
}
|
||||
|
||||
@@ -478,8 +478,15 @@ func (k8sHandler *K8sResourceHandler) setCloudProvider() error {
|
||||
// NoSchedule taint with empty value is usually applied to controlplane
|
||||
func isMasterNodeTaints(taints []v1.Taint) bool {
|
||||
for _, taint := range taints {
|
||||
if taint.Effect == v1.TaintEffectNoSchedule && taint.Value == "" {
|
||||
return true
|
||||
if taint.Effect == v1.TaintEffectNoSchedule {
|
||||
// NoSchedule taint with empty value is usually applied to controlplane
|
||||
if taint.Value == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if taint.Key == "node-role.kubernetes.io/control-plane" && taint.Value == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -14,264 +14,264 @@ import (
|
||||
)
|
||||
|
||||
func TestIsMasterNodeTaints(t *testing.T) {
|
||||
noTaintNode := `
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Node",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
|
||||
"node.alpha.kubernetes.io/ttl": "0",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||
},
|
||||
"creationTimestamp": "2022-05-16T10:52:32Z",
|
||||
"labels": {
|
||||
"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "danielg-minikube",
|
||||
"kubernetes.io/os": "linux",
|
||||
"minikube.k8s.io/commit": "3e64b11ed75e56e4898ea85f96b2e4af0301f43d",
|
||||
"minikube.k8s.io/name": "danielg-minikube",
|
||||
"minikube.k8s.io/updated_at": "2022_05_16T13_52_35_0700",
|
||||
"minikube.k8s.io/version": "v1.25.1",
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
"node-role.kubernetes.io/master": "",
|
||||
"node.kubernetes.io/exclude-from-external-load-balancers": ""
|
||||
},
|
||||
"name": "danielg-minikube",
|
||||
"resourceVersion": "9432",
|
||||
"uid": "fc4afcb6-4ca4-4038-ba54-5e16065a614a"
|
||||
},
|
||||
"spec": {
|
||||
"podCIDR": "10.244.0.0/24",
|
||||
"podCIDRs": [
|
||||
"10.244.0.0/24"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"addresses": [
|
||||
noTaintNodeJson := `
|
||||
{
|
||||
"address": "192.168.49.2",
|
||||
"type": "InternalIP"
|
||||
"apiVersion": "v1",
|
||||
"kind": "Node",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
|
||||
"node.alpha.kubernetes.io/ttl": "0",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||
},
|
||||
"creationTimestamp": "2022-05-16T10:52:32Z",
|
||||
"labels": {
|
||||
"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "danielg-minikube",
|
||||
"kubernetes.io/os": "linux",
|
||||
"minikube.k8s.io/commit": "3e64b11ed75e56e4898ea85f96b2e4af0301f43d",
|
||||
"minikube.k8s.io/name": "danielg-minikube",
|
||||
"minikube.k8s.io/updated_at": "2022_05_16T13_52_35_0700",
|
||||
"minikube.k8s.io/version": "v1.25.1",
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
"node-role.kubernetes.io/master": "",
|
||||
"node.kubernetes.io/exclude-from-external-load-balancers": ""
|
||||
},
|
||||
"name": "danielg-minikube",
|
||||
"resourceVersion": "9432",
|
||||
"uid": "fc4afcb6-4ca4-4038-ba54-5e16065a614a"
|
||||
},
|
||||
{
|
||||
"address": "danielg-minikube",
|
||||
"type": "Hostname"
|
||||
"spec": {
|
||||
"podCIDR": "10.244.0.0/24",
|
||||
"podCIDRs": [
|
||||
"10.244.0.0/24"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"addresses": [
|
||||
{
|
||||
"address": "192.168.49.2",
|
||||
"type": "InternalIP"
|
||||
},
|
||||
{
|
||||
"address": "danielg-minikube",
|
||||
"type": "Hostname"
|
||||
}
|
||||
],
|
||||
"allocatable": {
|
||||
"cpu": "4",
|
||||
"ephemeral-storage": "94850516Ki",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "10432976Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"capacity": {
|
||||
"cpu": "4",
|
||||
"ephemeral-storage": "94850516Ki",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "10432976Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has sufficient memory available",
|
||||
"reason": "KubeletHasSufficientMemory",
|
||||
"status": "False",
|
||||
"type": "MemoryPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has no disk pressure",
|
||||
"reason": "KubeletHasNoDiskPressure",
|
||||
"status": "False",
|
||||
"type": "DiskPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has sufficient PID available",
|
||||
"reason": "KubeletHasSufficientPID",
|
||||
"status": "False",
|
||||
"type": "PIDPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:45Z",
|
||||
"message": "kubelet is posting ready status",
|
||||
"reason": "KubeletReady",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
}
|
||||
],
|
||||
"daemonEndpoints": {
|
||||
"kubeletEndpoint": {
|
||||
"Port": 10250
|
||||
}
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"names": [
|
||||
"requarks/wiki@sha256:dd83fff15e77843ff934b25c28c865ac000edf7653e5d11adad1dd51df87439d"
|
||||
],
|
||||
"sizeBytes": 441083858
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"mariadb@sha256:821d0411208eaa88f9e1f0daccd1d534f88d19baf724eb9a2777cbedb10b6c66"
|
||||
],
|
||||
"sizeBytes": 400782682
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/etcd@sha256:64b9ea357325d5db9f8a723dcf503b5a449177b17ac87d69481e126bb724c263",
|
||||
"k8s.gcr.io/etcd:3.5.1-0"
|
||||
],
|
||||
"sizeBytes": 292558922
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"kubernetesui/dashboard@sha256:ec27f462cf1946220f5a9ace416a84a57c18f98c777876a8054405d1428cc92e",
|
||||
"kubernetesui/dashboard:v2.3.1"
|
||||
],
|
||||
"sizeBytes": 220033604
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-apiserver@sha256:f54681a71cce62cbc1b13ebb3dbf1d880f849112789811f98b6aebd2caa2f255",
|
||||
"k8s.gcr.io/kube-apiserver:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 135162256
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-controller-manager@sha256:a7ed87380108a2d811f0d392a3fe87546c85bc366e0d1e024dfa74eb14468604",
|
||||
"k8s.gcr.io/kube-controller-manager:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 124971684
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-proxy@sha256:e40f3a28721588affcf187f3f246d1e078157dabe274003eaa2957a83f7170c8",
|
||||
"k8s.gcr.io/kube-proxy:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 112327826
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/kubescape/kubescape@sha256:6196f766be50d94b45d903a911f5ee95ac99bc392a1324c3e063bec41efd98ba",
|
||||
"quay.io/kubescape/kubescape:v2.0.153"
|
||||
],
|
||||
"sizeBytes": 110345054
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d"
|
||||
],
|
||||
"sizeBytes": 109129446
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/action-trigger@sha256:b93707d10ff86aac8dfa42ad37192d6bcf9aceeb4321b21756e438389c26e07c",
|
||||
"quay.io/armosec/action-trigger:v0.0.5"
|
||||
],
|
||||
"sizeBytes": 65127067
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/images-vulnerabilities-scan@sha256:a5f9ddc04a7fdce6d52ef85a21f0de567d8e04d418c2bc5bf5d72b151c997625",
|
||||
"quay.io/armosec/images-vulnerabilities-scan:v0.0.7"
|
||||
],
|
||||
"sizeBytes": 61446712
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/images-vulnerabilities-scan@sha256:2f879858da89f6542e3223fb18d6d793810cc2ad6e398b66776475e4218b6af5",
|
||||
"quay.io/armosec/images-vulnerabilities-scan:v0.0.8"
|
||||
],
|
||||
"sizeBytes": 61446528
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/cluster-collector@sha256:2c4f733d09f7f4090ace04585230bdfacbbc29a3ade38a2e1233d2c0f730d9b6",
|
||||
"quay.io/armosec/cluster-collector:v0.0.9"
|
||||
],
|
||||
"sizeBytes": 53699576
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-scheduler@sha256:8be4eb1593cf9ff2d91b44596633b7815a3753696031a1eb4273d1b39427fa8c",
|
||||
"k8s.gcr.io/kube-scheduler:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 53488305
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/coredns/coredns@sha256:5b6ec0d6de9baaf3e92d0f66cd96a25b9edbce8716f5f15dcd1a616b3abd590e",
|
||||
"k8s.gcr.io/coredns/coredns:v1.8.6"
|
||||
],
|
||||
"sizeBytes": 46829283
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"kubernetesui/metrics-scraper@sha256:36d5b3f60e1a144cc5ada820910535074bdf5cf73fb70d1ff1681537eef4e172",
|
||||
"kubernetesui/metrics-scraper:v1.0.7"
|
||||
],
|
||||
"sizeBytes": 34446077
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944",
|
||||
"gcr.io/k8s-minikube/storage-provisioner:v5"
|
||||
],
|
||||
"sizeBytes": 31465472
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/notification-server@sha256:b6e9b296cd53bd3b2b42c516d8ab43db998acff1124a57aff8d66b3dd7881979",
|
||||
"quay.io/armosec/notification-server:v0.0.3"
|
||||
],
|
||||
"sizeBytes": 20209940
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/kubescape/host-scanner@sha256:82139d2561039726be060df2878ef023c59df7c536fbd7f6d766af5a99569fee",
|
||||
"quay.io/kubescape/host-scanner:latest"
|
||||
],
|
||||
"sizeBytes": 11796788
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/pause@sha256:3d380ca8864549e74af4b29c10f9cb0956236dfb01c40ca076fb6c37253234db",
|
||||
"k8s.gcr.io/pause:3.6"
|
||||
],
|
||||
"sizeBytes": 682696
|
||||
}
|
||||
],
|
||||
"nodeInfo": {
|
||||
"architecture": "amd64",
|
||||
"bootID": "828cbe73-120b-43cf-aae0-9e2d15b8c873",
|
||||
"containerRuntimeVersion": "docker://20.10.12",
|
||||
"kernelVersion": "5.13.0-40-generic",
|
||||
"kubeProxyVersion": "v1.23.1",
|
||||
"kubeletVersion": "v1.23.1",
|
||||
"machineID": "8de776e053e140d6a14c2d2def3d6bb8",
|
||||
"operatingSystem": "linux",
|
||||
"osImage": "Ubuntu 20.04.2 LTS",
|
||||
"systemUUID": "da12dc19-10bf-4033-a440-2d9aa33d6fe3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allocatable": {
|
||||
"cpu": "4",
|
||||
"ephemeral-storage": "94850516Ki",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "10432976Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"capacity": {
|
||||
"cpu": "4",
|
||||
"ephemeral-storage": "94850516Ki",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "10432976Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has sufficient memory available",
|
||||
"reason": "KubeletHasSufficientMemory",
|
||||
"status": "False",
|
||||
"type": "MemoryPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has no disk pressure",
|
||||
"reason": "KubeletHasNoDiskPressure",
|
||||
"status": "False",
|
||||
"type": "DiskPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:29Z",
|
||||
"message": "kubelet has sufficient PID available",
|
||||
"reason": "KubeletHasSufficientPID",
|
||||
"status": "False",
|
||||
"type": "PIDPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
|
||||
"lastTransitionTime": "2022-05-16T10:52:45Z",
|
||||
"message": "kubelet is posting ready status",
|
||||
"reason": "KubeletReady",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
}
|
||||
],
|
||||
"daemonEndpoints": {
|
||||
"kubeletEndpoint": {
|
||||
"Port": 10250
|
||||
}
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"names": [
|
||||
"requarks/wiki@sha256:dd83fff15e77843ff934b25c28c865ac000edf7653e5d11adad1dd51df87439d"
|
||||
],
|
||||
"sizeBytes": 441083858
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"mariadb@sha256:821d0411208eaa88f9e1f0daccd1d534f88d19baf724eb9a2777cbedb10b6c66"
|
||||
],
|
||||
"sizeBytes": 400782682
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/etcd@sha256:64b9ea357325d5db9f8a723dcf503b5a449177b17ac87d69481e126bb724c263",
|
||||
"k8s.gcr.io/etcd:3.5.1-0"
|
||||
],
|
||||
"sizeBytes": 292558922
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"kubernetesui/dashboard@sha256:ec27f462cf1946220f5a9ace416a84a57c18f98c777876a8054405d1428cc92e",
|
||||
"kubernetesui/dashboard:v2.3.1"
|
||||
],
|
||||
"sizeBytes": 220033604
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-apiserver@sha256:f54681a71cce62cbc1b13ebb3dbf1d880f849112789811f98b6aebd2caa2f255",
|
||||
"k8s.gcr.io/kube-apiserver:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 135162256
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-controller-manager@sha256:a7ed87380108a2d811f0d392a3fe87546c85bc366e0d1e024dfa74eb14468604",
|
||||
"k8s.gcr.io/kube-controller-manager:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 124971684
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-proxy@sha256:e40f3a28721588affcf187f3f246d1e078157dabe274003eaa2957a83f7170c8",
|
||||
"k8s.gcr.io/kube-proxy:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 112327826
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/kubescape/kubescape@sha256:6196f766be50d94b45d903a911f5ee95ac99bc392a1324c3e063bec41efd98ba",
|
||||
"quay.io/kubescape/kubescape:v2.0.153"
|
||||
],
|
||||
"sizeBytes": 110345054
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d"
|
||||
],
|
||||
"sizeBytes": 109129446
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/action-trigger@sha256:b93707d10ff86aac8dfa42ad37192d6bcf9aceeb4321b21756e438389c26e07c",
|
||||
"quay.io/armosec/action-trigger:v0.0.5"
|
||||
],
|
||||
"sizeBytes": 65127067
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/images-vulnerabilities-scan@sha256:a5f9ddc04a7fdce6d52ef85a21f0de567d8e04d418c2bc5bf5d72b151c997625",
|
||||
"quay.io/armosec/images-vulnerabilities-scan:v0.0.7"
|
||||
],
|
||||
"sizeBytes": 61446712
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/images-vulnerabilities-scan@sha256:2f879858da89f6542e3223fb18d6d793810cc2ad6e398b66776475e4218b6af5",
|
||||
"quay.io/armosec/images-vulnerabilities-scan:v0.0.8"
|
||||
],
|
||||
"sizeBytes": 61446528
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/cluster-collector@sha256:2c4f733d09f7f4090ace04585230bdfacbbc29a3ade38a2e1233d2c0f730d9b6",
|
||||
"quay.io/armosec/cluster-collector:v0.0.9"
|
||||
],
|
||||
"sizeBytes": 53699576
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/kube-scheduler@sha256:8be4eb1593cf9ff2d91b44596633b7815a3753696031a1eb4273d1b39427fa8c",
|
||||
"k8s.gcr.io/kube-scheduler:v1.23.1"
|
||||
],
|
||||
"sizeBytes": 53488305
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/coredns/coredns@sha256:5b6ec0d6de9baaf3e92d0f66cd96a25b9edbce8716f5f15dcd1a616b3abd590e",
|
||||
"k8s.gcr.io/coredns/coredns:v1.8.6"
|
||||
],
|
||||
"sizeBytes": 46829283
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"kubernetesui/metrics-scraper@sha256:36d5b3f60e1a144cc5ada820910535074bdf5cf73fb70d1ff1681537eef4e172",
|
||||
"kubernetesui/metrics-scraper:v1.0.7"
|
||||
],
|
||||
"sizeBytes": 34446077
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944",
|
||||
"gcr.io/k8s-minikube/storage-provisioner:v5"
|
||||
],
|
||||
"sizeBytes": 31465472
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/armosec/notification-server@sha256:b6e9b296cd53bd3b2b42c516d8ab43db998acff1124a57aff8d66b3dd7881979",
|
||||
"quay.io/armosec/notification-server:v0.0.3"
|
||||
],
|
||||
"sizeBytes": 20209940
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"quay.io/kubescape/host-scanner@sha256:82139d2561039726be060df2878ef023c59df7c536fbd7f6d766af5a99569fee",
|
||||
"quay.io/kubescape/host-scanner:latest"
|
||||
],
|
||||
"sizeBytes": 11796788
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"k8s.gcr.io/pause@sha256:3d380ca8864549e74af4b29c10f9cb0956236dfb01c40ca076fb6c37253234db",
|
||||
"k8s.gcr.io/pause:3.6"
|
||||
],
|
||||
"sizeBytes": 682696
|
||||
}
|
||||
],
|
||||
"nodeInfo": {
|
||||
"architecture": "amd64",
|
||||
"bootID": "828cbe73-120b-43cf-aae0-9e2d15b8c873",
|
||||
"containerRuntimeVersion": "docker://20.10.12",
|
||||
"kernelVersion": "5.13.0-40-generic",
|
||||
"kubeProxyVersion": "v1.23.1",
|
||||
"kubeletVersion": "v1.23.1",
|
||||
"machineID": "8de776e053e140d6a14c2d2def3d6bb8",
|
||||
"operatingSystem": "linux",
|
||||
"osImage": "Ubuntu 20.04.2 LTS",
|
||||
"systemUUID": "da12dc19-10bf-4033-a440-2d9aa33d6fe3"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
var l v1.Node
|
||||
_ = json.Unmarshal([]byte(noTaintNode), &l)
|
||||
assert.False(t, isMasterNodeTaints(l.Spec.Taints))
|
||||
`
|
||||
var noTaintNode v1.Node
|
||||
_ = json.Unmarshal([]byte(noTaintNodeJson), &noTaintNode)
|
||||
assert.False(t, isMasterNodeTaints(noTaintNode.Spec.Taints))
|
||||
|
||||
taintNode :=
|
||||
taintNodeJson :=
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
@@ -532,8 +532,60 @@ func TestIsMasterNodeTaints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
`
|
||||
_ = json.Unmarshal([]byte(taintNode), &l)
|
||||
assert.True(t, isMasterNodeTaints(l.Spec.Taints))
|
||||
var taintNode v1.Node
|
||||
_ = json.Unmarshal([]byte(taintNodeJson), &taintNode)
|
||||
assert.True(t, isMasterNodeTaints(taintNode.Spec.Taints))
|
||||
|
||||
taintNodeJson1 :=
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Node",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
|
||||
"node.alpha.kubernetes.io/ttl": "0",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||
},
|
||||
"creationTimestamp": "2022-05-16T10:52:32Z",
|
||||
"labels": {
|
||||
"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "danielg-minikube",
|
||||
"kubernetes.io/os": "linux",
|
||||
"minikube.k8s.io/commit": "3e64b11ed75e56e4898ea85f96b2e4af0301f43d",
|
||||
"minikube.k8s.io/name": "danielg-minikube",
|
||||
"minikube.k8s.io/updated_at": "2022_05_16T13_52_35_0700",
|
||||
"minikube.k8s.io/version": "v1.25.1",
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
"node-role.kubernetes.io/master": "",
|
||||
"node.kubernetes.io/exclude-from-external-load-balancers": ""
|
||||
},
|
||||
"name": "danielg-minikube",
|
||||
"resourceVersion": "9871",
|
||||
"uid": "fc4afcb6-4ca4-4038-ba54-5e16065a614a"
|
||||
},
|
||||
"spec": {
|
||||
"podCIDR": "10.244.0.0/24",
|
||||
"podCIDRs": [
|
||||
"10.244.0.0/24"
|
||||
],
|
||||
"taints": [
|
||||
{
|
||||
"effect": "NoSchedule",
|
||||
"key": "node-role.kubernetes.io/control-plane",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
`
|
||||
|
||||
var taintNode1 v1.Node
|
||||
_ = json.Unmarshal([]byte(taintNodeJson1), &taintNode1)
|
||||
assert.True(t, isMasterNodeTaints(taintNode1.Spec.Taints))
|
||||
}
|
||||
|
||||
func TestSetMapNamespaceToNumOfResources(t *testing.T) {
|
||||
|
||||
@@ -9,7 +9,7 @@ const (
|
||||
emptySpace = " "
|
||||
middleItem = "├── "
|
||||
continueItem = "│ "
|
||||
lastItem = "└── "
|
||||
lastItem = "╰── "
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -66,7 +66,7 @@ func (t *tree) Items() []Tree {
|
||||
return t.items
|
||||
}
|
||||
|
||||
// Print returns an visual representation of the tree
|
||||
// Print returns a visual representation of the tree
|
||||
func (t *tree) Print() string {
|
||||
return newPrinter().Print(t)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestTreePrint(t *testing.T) {
|
||||
tree: SimpleTreeMock(),
|
||||
want: "root\n" +
|
||||
"├── child1\n" +
|
||||
"└── child2\n",
|
||||
"╰── child2\n",
|
||||
},
|
||||
{
|
||||
name: "SimpleTreeWithLinesMock",
|
||||
@@ -42,36 +42,36 @@ func TestTreePrint(t *testing.T) {
|
||||
"├── child3\n" +
|
||||
"│ Line2\n" +
|
||||
"│ Line3\n" +
|
||||
"└── child4\n",
|
||||
"╰── child4\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeMock1",
|
||||
tree: SubTreeMock1(),
|
||||
want: "root\n" +
|
||||
"└── child1\n" +
|
||||
" └── child1.1\n",
|
||||
"╰── child1\n" +
|
||||
" ╰── child1.1\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeMock2",
|
||||
tree: SubTreeMock2(),
|
||||
want: "root\n" +
|
||||
"├── child1\n" +
|
||||
"│ └── child1.1\n" +
|
||||
"│ ╰── child1.1\n" +
|
||||
"├── child2\n" +
|
||||
"└── child3\n" +
|
||||
" └── child3.1\n",
|
||||
"╰── child3\n" +
|
||||
" ╰── child3.1\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeWithLinesMock",
|
||||
tree: SubTreeWithLinesMock(),
|
||||
want: "root\n" +
|
||||
"├── child1\n" +
|
||||
"│ └── child1.1\n" +
|
||||
"│ ╰── child1.1\n" +
|
||||
"│ Line2\n" +
|
||||
"│ Line3\n" +
|
||||
"├── child2\n" +
|
||||
"└── child3\n" +
|
||||
" └── child3.1\n" +
|
||||
"╰── child3\n" +
|
||||
" ╰── child3.1\n" +
|
||||
" Line2\n" +
|
||||
" Line3\n",
|
||||
},
|
||||
@@ -85,8 +85,8 @@ func TestTreePrint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrintText_LastTree(t *testing.T) {
|
||||
inputText := "Root\n├── Child1\n└── Child2"
|
||||
expectedOutput := "└── Root\n ├── Child1\n └── Child2\n"
|
||||
inputText := "Root\n├── Child1\n╰── Child2"
|
||||
expectedOutput := "╰── Root\n ├── Child1\n ╰── Child2\n"
|
||||
|
||||
result := p.printText(inputText, []bool{}, true)
|
||||
|
||||
@@ -94,8 +94,8 @@ func TestPrintText_LastTree(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrintText_NotLastTree(t *testing.T) {
|
||||
inputText := "Root\n├── Child1\n└── Child2"
|
||||
expectedOutput := "├── Root\n│ ├── Child1\n│ └── Child2\n"
|
||||
inputText := "Root\n├── Child1\n╰── Child2"
|
||||
expectedOutput := "├── Root\n│ ├── Child1\n│ ╰── Child2\n"
|
||||
|
||||
result := p.printText(inputText, []bool{}, false)
|
||||
|
||||
@@ -122,7 +122,7 @@ func Test_printer_printItems(t *testing.T) {
|
||||
name: "SimpleTreeMock",
|
||||
tree: SimpleTreeMock(),
|
||||
want: "├── child1\n" +
|
||||
"└── child2\n",
|
||||
"╰── child2\n",
|
||||
},
|
||||
{
|
||||
name: "SimpleTreeWithLinesMock",
|
||||
@@ -132,33 +132,33 @@ func Test_printer_printItems(t *testing.T) {
|
||||
"├── child3\n" +
|
||||
"│ Line2\n" +
|
||||
"│ Line3\n" +
|
||||
"└── child4\n",
|
||||
"╰── child4\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeMock1",
|
||||
tree: SubTreeMock1(),
|
||||
want: "└── child1\n" +
|
||||
" └── child1.1\n",
|
||||
want: "╰── child1\n" +
|
||||
" ╰── child1.1\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeMock2",
|
||||
tree: SubTreeMock2(),
|
||||
want: "├── child1\n" +
|
||||
"│ └── child1.1\n" +
|
||||
"│ ╰── child1.1\n" +
|
||||
"├── child2\n" +
|
||||
"└── child3\n" +
|
||||
" └── child3.1\n",
|
||||
"╰── child3\n" +
|
||||
" ╰── child3.1\n",
|
||||
},
|
||||
{
|
||||
name: "SubTreeWithLinesMock",
|
||||
tree: SubTreeWithLinesMock(),
|
||||
want: "├── child1\n" +
|
||||
"│ └── child1.1\n" +
|
||||
"│ ╰── child1.1\n" +
|
||||
"│ Line2\n" +
|
||||
"│ Line3\n" +
|
||||
"├── child2\n" +
|
||||
"└── child3\n" +
|
||||
" └── child3.1\n" +
|
||||
"╰── child3\n" +
|
||||
" ╰── child3.1\n" +
|
||||
" Line2\n" +
|
||||
" Line3\n",
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ func (prettyPrinter *PrettyPrinter) printAttackTracks(opaSessionObj *cautils.OPA
|
||||
})
|
||||
|
||||
for i := 0; i < topResourceCount && i < len(resources); i++ {
|
||||
fmt.Fprintf(prettyPrinter.writer, "\n"+getSeparator("^")+"\n")
|
||||
fmt.Fprintf(prettyPrinter.writer, "\n%s\n", getSeparator("^"))
|
||||
resource := resources[i]
|
||||
resourceObj := opaSessionObj.AllResources[resource.ResourceID]
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
grypejson "github.com/anchore/grype/grype/presenter/json"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"k8s.io/utils/strings/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -70,17 +70,10 @@ func (jp *JsonPrinter) convertToImageScanSummary(imageScanData []cautils.ImageSc
|
||||
imageScanSummary.Images = append(imageScanSummary.Images, imageScanData[i].Image)
|
||||
}
|
||||
|
||||
presenterConfig := imageScanData[i].PresenterConfig
|
||||
doc, err := models.NewDocument(clio.Identification{}, presenterConfig.Packages, presenterConfig.Context, presenterConfig.Matches, presenterConfig.IgnoredMatches, presenterConfig.MetadataProvider, nil, presenterConfig.DBStatus)
|
||||
if err != nil {
|
||||
logger.L().Error(fmt.Sprintf("failed to create document for image: %v", imageScanData[i].Image), helpers.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
CVEs := extractCVEs(doc.Matches)
|
||||
CVEs := extractCVEs(imageScanData[i].Matches)
|
||||
imageScanSummary.CVEs = append(imageScanSummary.CVEs, CVEs...)
|
||||
|
||||
setPkgNameToScoreMap(doc.Matches, imageScanSummary.PackageScores)
|
||||
setPkgNameToScoreMap(imageScanData[i].Matches, imageScanSummary.PackageScores)
|
||||
|
||||
setSeverityToSummaryMap(CVEs, imageScanSummary.MapsSeverityToSummary)
|
||||
}
|
||||
@@ -92,9 +85,15 @@ func (jp *JsonPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.O
|
||||
var err error
|
||||
|
||||
if opaSessionObj != nil {
|
||||
err = printConfigurationsScanning(opaSessionObj, ctx, imageScanData, jp)
|
||||
err = printConfigurationsScanning(opaSessionObj, imageScanData, jp)
|
||||
} else if imageScanData != nil {
|
||||
err = jp.PrintImageScan(ctx, imageScanData[0].PresenterConfig)
|
||||
model, err2 := models.NewDocument(clio.Identification{}, imageScanData[0].Packages, imageScanData[0].Context,
|
||||
*imageScanData[0].RemainingMatches, imageScanData[0].IgnoredMatches, imageScanData[0].VulnerabilityProvider, nil, nil, models.DefaultSortStrategy, false)
|
||||
if err2 != nil {
|
||||
logger.L().Ctx(ctx).Error("failed to create document: %w", helpers.Error(err))
|
||||
return
|
||||
}
|
||||
err = grypejson.NewPresenter(models.PresenterConfig{Document: model, SBOM: imageScanData[0].SBOM}).Present(jp.writer)
|
||||
} else {
|
||||
err = fmt.Errorf("no data provided")
|
||||
}
|
||||
@@ -107,7 +106,7 @@ func (jp *JsonPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.O
|
||||
printer.LogOutputFile(jp.writer.Name())
|
||||
}
|
||||
|
||||
func printConfigurationsScanning(opaSessionObj *cautils.OPASessionObj, ctx context.Context, imageScanData []cautils.ImageScanData, jp *JsonPrinter) error {
|
||||
func printConfigurationsScanning(opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData, jp *JsonPrinter) error {
|
||||
|
||||
if imageScanData != nil {
|
||||
imageScanSummary, err := jp.convertToImageScanSummary(imageScanData)
|
||||
@@ -121,7 +120,12 @@ func printConfigurationsScanning(opaSessionObj *cautils.OPASessionObj, ctx conte
|
||||
opaSessionObj.Report.SummaryDetails.Vulnerabilities.Images = imageScanSummary.Images
|
||||
}
|
||||
|
||||
r, err := json.Marshal(FinalizeResults(opaSessionObj))
|
||||
// Convert to PostureReportWithSeverity to add severity field to controls
|
||||
// and extract specified labels from workloads
|
||||
finalizedReport := FinalizeResults(opaSessionObj)
|
||||
reportWithSeverity := ConvertToPostureReportWithSeverityAndLabels(finalizedReport, opaSessionObj.LabelsToCopy, opaSessionObj.AllResources)
|
||||
|
||||
r, err := json.Marshal(reportWithSeverity)
|
||||
_, err = jp.writer.Write(r)
|
||||
|
||||
return err
|
||||
@@ -168,14 +172,6 @@ func convertToReportSummary(input map[string]*imageprinter.SeveritySummary) map[
|
||||
return output
|
||||
}
|
||||
|
||||
func (jp *JsonPrinter) PrintImageScan(ctx context.Context, scanResults *models.PresenterConfig) error {
|
||||
if scanResults == nil {
|
||||
return fmt.Errorf("no image vulnerability data provided")
|
||||
}
|
||||
pres := presenter.GetPresenter("json", "", false, *scanResults)
|
||||
return pres.Present(jp.writer)
|
||||
}
|
||||
|
||||
func (jp *JsonPrinter) PrintNextSteps() {
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -192,3 +194,154 @@ func TestConvertToReportSummary(t *testing.T) {
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestEnrichControlsWithSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scoreFactor float32
|
||||
wantSeverity string
|
||||
}{
|
||||
{
|
||||
name: "Critical severity",
|
||||
scoreFactor: 9.0,
|
||||
wantSeverity: "Critical",
|
||||
},
|
||||
{
|
||||
name: "High severity",
|
||||
scoreFactor: 8.0,
|
||||
wantSeverity: "High",
|
||||
},
|
||||
{
|
||||
name: "Medium severity",
|
||||
scoreFactor: 6.0,
|
||||
wantSeverity: "Medium",
|
||||
},
|
||||
{
|
||||
name: "Low severity",
|
||||
scoreFactor: 3.0,
|
||||
wantSeverity: "Low",
|
||||
},
|
||||
{
|
||||
name: "Unknown severity",
|
||||
scoreFactor: 0.0,
|
||||
wantSeverity: "Unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
controls := reportsummary.ControlSummaries{
|
||||
"C-0001": reportsummary.ControlSummary{
|
||||
ControlID: "C-0001",
|
||||
Name: "Test Control",
|
||||
ScoreFactor: tt.scoreFactor,
|
||||
},
|
||||
}
|
||||
|
||||
enrichedControls := enrichControlsWithSeverity(controls)
|
||||
|
||||
assert.Equal(t, 1, len(enrichedControls))
|
||||
assert.Equal(t, tt.wantSeverity, enrichedControls["C-0001"].Severity)
|
||||
assert.Equal(t, "Test Control", enrichedControls["C-0001"].Name)
|
||||
assert.Equal(t, tt.scoreFactor, enrichedControls["C-0001"].ScoreFactor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToPostureReportWithSeverity(t *testing.T) {
|
||||
// Create a mock PostureReport with controls having different severity levels
|
||||
mockReport := reportsummary.MockSummaryDetails()
|
||||
|
||||
// Get the controls from mock data
|
||||
controls := mockReport.Controls
|
||||
|
||||
// Create a minimal PostureReport
|
||||
report := &reporthandlingv2.PostureReport{
|
||||
SummaryDetails: *mockReport,
|
||||
}
|
||||
|
||||
// Convert to PostureReportWithSeverity
|
||||
reportWithSeverity := ConvertToPostureReportWithSeverity(report)
|
||||
|
||||
// Verify controls have severity field
|
||||
assert.NotNil(t, reportWithSeverity)
|
||||
assert.NotNil(t, reportWithSeverity.SummaryDetails.Controls)
|
||||
|
||||
// Verify each control in the original report has a corresponding enriched control with severity
|
||||
for controlID, control := range controls {
|
||||
enrichedControl, exists := reportWithSeverity.SummaryDetails.Controls[controlID]
|
||||
assert.True(t, exists, "Control %s should exist in enriched controls", controlID)
|
||||
assert.NotEmpty(t, enrichedControl.Severity, "Severity should not be empty for control %s", controlID)
|
||||
assert.Equal(t, control.ControlID, enrichedControl.ControlID, "Control ID should match")
|
||||
assert.Equal(t, control.ScoreFactor, enrichedControl.ScoreFactor, "ScoreFactor should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToPostureReportWithSeverityNilCheck(t *testing.T) {
|
||||
// Test that nil report returns nil
|
||||
result := ConvertToPostureReportWithSeverity(nil)
|
||||
assert.Nil(t, result, "Converting nil report should return nil")
|
||||
}
|
||||
|
||||
func TestEnrichResultsWithSeverity(t *testing.T) {
|
||||
// Create mock control summaries
|
||||
controlSummaries := reportsummary.ControlSummaries{
|
||||
"C-0001": reportsummary.ControlSummary{
|
||||
ControlID: "C-0001",
|
||||
Name: "Test Control High",
|
||||
ScoreFactor: 8.0,
|
||||
},
|
||||
"C-0002": reportsummary.ControlSummary{
|
||||
ControlID: "C-0002",
|
||||
Name: "Test Control Medium",
|
||||
ScoreFactor: 6.0,
|
||||
},
|
||||
}
|
||||
|
||||
// Create mock results with associated controls
|
||||
results := []resourcesresults.Result{
|
||||
{
|
||||
ResourceID: "test-resource-1",
|
||||
AssociatedControls: []resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "C-0001",
|
||||
Name: "Test Control High",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceID: "test-resource-2",
|
||||
AssociatedControls: []resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "C-0002",
|
||||
Name: "Test Control Medium",
|
||||
},
|
||||
{
|
||||
ControlID: "C-0003", // Not in control summaries
|
||||
Name: "Unknown Control",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Enrich results with severity
|
||||
enrichedResults := enrichResultsWithSeverity(results, controlSummaries)
|
||||
|
||||
// Verify results structure
|
||||
assert.Equal(t, 2, len(enrichedResults))
|
||||
|
||||
// Verify first result
|
||||
assert.Equal(t, "test-resource-1", enrichedResults[0].ResourceID)
|
||||
assert.Equal(t, 1, len(enrichedResults[0].AssociatedControls))
|
||||
assert.Equal(t, "High", enrichedResults[0].AssociatedControls[0].Severity)
|
||||
assert.Equal(t, "C-0001", enrichedResults[0].AssociatedControls[0].ControlID)
|
||||
|
||||
// Verify second result
|
||||
assert.Equal(t, "test-resource-2", enrichedResults[1].ResourceID)
|
||||
assert.Equal(t, 2, len(enrichedResults[1].AssociatedControls))
|
||||
assert.Equal(t, "Medium", enrichedResults[1].AssociatedControls[0].Severity)
|
||||
assert.Equal(t, "C-0002", enrichedResults[1].AssociatedControls[0].ControlID)
|
||||
// Verify unknown control gets "Unknown" severity
|
||||
assert.Equal(t, "Unknown", enrichedResults[1].AssociatedControls[1].Severity)
|
||||
assert.Equal(t, "C-0003", enrichedResults[1].AssociatedControls[1].ControlID)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/enescakir/emoji"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/kubescape/opa-utils/objectsenvelopes"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"k8s.io/utils/strings/slices"
|
||||
)
|
||||
|
||||
@@ -90,17 +89,10 @@ func (pp *PrettyPrinter) convertToImageScanSummary(imageScanData []cautils.Image
|
||||
imageScanSummary.Images = append(imageScanSummary.Images, imageScanData[i].Image)
|
||||
}
|
||||
|
||||
presenterConfig := imageScanData[i].PresenterConfig
|
||||
doc, err := models.NewDocument(clio.Identification{}, presenterConfig.Packages, presenterConfig.Context, presenterConfig.Matches, presenterConfig.IgnoredMatches, presenterConfig.MetadataProvider, nil, presenterConfig.DBStatus)
|
||||
if err != nil {
|
||||
logger.L().Error(fmt.Sprintf("failed to create document for image: %v", imageScanData[i].Image), helpers.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
CVEs := extractCVEs(doc.Matches)
|
||||
CVEs := extractCVEs(imageScanData[i].Matches)
|
||||
imageScanSummary.CVEs = append(imageScanSummary.CVEs, CVEs...)
|
||||
|
||||
setPkgNameToScoreMap(doc.Matches, imageScanSummary.PackageScores)
|
||||
setPkgNameToScoreMap(imageScanData[i].Matches, imageScanSummary.PackageScores)
|
||||
|
||||
setSeverityToSummaryMap(CVEs, imageScanSummary.MapsSeverityToSummary)
|
||||
}
|
||||
@@ -121,9 +113,8 @@ func (pp *PrettyPrinter) ActionPrint(_ context.Context, opaSessionObj *cautils.O
|
||||
if opaSessionObj != nil {
|
||||
// TODO line is currently printed on framework scan only
|
||||
if isPrintSeparatorType(pp.scanType) {
|
||||
fmt.Fprintf(pp.writer, "\n"+
|
||||
gchalk.WithAnsi256(238).Bold(fmt.Sprintf("%s\n", strings.Repeat("─", 50)))+
|
||||
"\n")
|
||||
fmt.Fprintf(pp.writer, "\n%s\n\n",
|
||||
gchalk.WithAnsi256(238).Bold(strings.Repeat("─", 50)))
|
||||
} else {
|
||||
fmt.Fprintf(pp.writer, "\n")
|
||||
}
|
||||
@@ -174,20 +165,20 @@ func (pp *PrettyPrinter) printHeader(opaSessionObj *cautils.OPASessionObj) {
|
||||
} else if pp.scanType == cautils.ScanTypeWorkload {
|
||||
cautils.InfoDisplay(pp.writer, "Workload security posture overview for:\n")
|
||||
ns := opaSessionObj.SingleResourceScan.GetNamespace()
|
||||
rows := [][]string{}
|
||||
var rows []table.Row
|
||||
if ns != "" {
|
||||
rows = append(rows, []string{"Namespace", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetNamespace())})
|
||||
rows = append(rows, table.Row{"Namespace", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetNamespace())})
|
||||
}
|
||||
rows = append(rows, []string{"Kind", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetKind())})
|
||||
rows = append(rows, []string{"Name", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetName())})
|
||||
rows = append(rows, table.Row{"Kind", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetKind())})
|
||||
rows = append(rows, table.Row{"Name", gchalk.WithBrightWhite().Bold(opaSessionObj.SingleResourceScan.GetName())})
|
||||
|
||||
table := tablewriter.NewWriter(pp.writer)
|
||||
tableWriter := table.NewWriter()
|
||||
tableWriter.SetOutputMirror(pp.writer)
|
||||
|
||||
table.SetColumnAlignment([]int{tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT})
|
||||
table.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
table.AppendBulk(rows)
|
||||
tableWriter.SetColumnConfigs([]table.ColumnConfig{{Number: 1, Align: text.AlignRight}, {Number: 2, Align: text.AlignLeft}})
|
||||
tableWriter.AppendRows(rows)
|
||||
|
||||
table.Render()
|
||||
tableWriter.Render()
|
||||
|
||||
cautils.SimpleDisplay(pp.writer, "\nIn this overview, Kubescape shows you a summary of the security posture of a workload, including key controls that apply to its configuration, and the vulnerability status of the container image.\n\n\n")
|
||||
}
|
||||
@@ -209,7 +200,7 @@ func (pp *PrettyPrinter) SetWriter(ctx context.Context, outputFile string) {
|
||||
pp.SetMainPrinter()
|
||||
}
|
||||
|
||||
func (pp *PrettyPrinter) Score(score float32) {
|
||||
func (pp *PrettyPrinter) Score(_ float32) {
|
||||
}
|
||||
|
||||
func (pp *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries, allResources map[string]workloadinterface.IMetadata, sortedControlIDs [][]string) {
|
||||
@@ -218,12 +209,12 @@ func (pp *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries,
|
||||
controlSummary := controls.GetControl(reportsummary.EControlCriteriaID, c) // summaryDetails.Controls ListControls().All() Controls.GetControl(ca)
|
||||
pp.printTitle(controlSummary)
|
||||
pp.printResources(controlSummary, allResources)
|
||||
pp.printSummary(c, controlSummary)
|
||||
pp.printSummary(controlSummary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSummary reportsummary.IControlSummary) {
|
||||
func (prettyPrinter *PrettyPrinter) printSummary(controlSummary reportsummary.IControlSummary) {
|
||||
cautils.SimpleDisplay(prettyPrinter.writer, "Summary - ")
|
||||
cautils.SuccessDisplay(prettyPrinter.writer, "Passed:%v ", controlSummary.NumberOfResources().Passed())
|
||||
cautils.WarningDisplay(prettyPrinter.writer, "Action Required:%v ", controlSummary.NumberOfResources().Skipped())
|
||||
|
||||
@@ -3,15 +3,15 @@ package configurationprinter
|
||||
import (
|
||||
"io"
|
||||
|
||||
"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"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
const (
|
||||
docsPrefix = "https://hub.armosec.io/docs"
|
||||
docsPrefix = "https://kubescape.io/docs/controls"
|
||||
scanControlPrefix = "$ kubescape scan control"
|
||||
controlNameHeader = "Control name"
|
||||
statusHeader = ""
|
||||
@@ -21,15 +21,15 @@ const (
|
||||
)
|
||||
|
||||
// initializes the table headers and column alignments based on the category type
|
||||
func initCategoryTableData(categoryType CategoryType) ([]string, []int) {
|
||||
func initCategoryTableData(categoryType CategoryType) (table.Row, []table.ColumnConfig) {
|
||||
if categoryType == TypeCounting {
|
||||
return getCategoryCountingTypeHeaders(), getCountingTypeAlignments()
|
||||
}
|
||||
return getCategoryStatusTypeHeaders(), getStatusTypeAlignments()
|
||||
}
|
||||
|
||||
func getCategoryStatusTypeHeaders() []string {
|
||||
headers := make([]string, 3)
|
||||
func getCategoryStatusTypeHeaders() table.Row {
|
||||
headers := make(table.Row, 3)
|
||||
headers[0] = statusHeader
|
||||
headers[1] = controlNameHeader
|
||||
headers[2] = docsHeader
|
||||
@@ -37,8 +37,8 @@ func getCategoryStatusTypeHeaders() []string {
|
||||
return headers
|
||||
}
|
||||
|
||||
func getCategoryCountingTypeHeaders() []string {
|
||||
headers := make([]string, 3)
|
||||
func getCategoryCountingTypeHeaders() table.Row {
|
||||
headers := make(table.Row, 3)
|
||||
headers[0] = controlNameHeader
|
||||
headers[1] = resourcesHeader
|
||||
headers[2] = runHeader
|
||||
@@ -46,16 +46,16 @@ func getCategoryCountingTypeHeaders() []string {
|
||||
return headers
|
||||
}
|
||||
|
||||
func getStatusTypeAlignments() []int {
|
||||
return []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER}
|
||||
func getStatusTypeAlignments() []table.ColumnConfig {
|
||||
return []table.ColumnConfig{{Number: 1, Align: text.AlignCenter}, {Number: 2, Align: text.AlignLeft}, {Number: 3, Align: text.AlignCenter}}
|
||||
}
|
||||
|
||||
func getCountingTypeAlignments() []int {
|
||||
return []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT}
|
||||
func getCountingTypeAlignments() []table.ColumnConfig {
|
||||
return []table.ColumnConfig{{Number: 1, Align: text.AlignLeft}, {Number: 2, Align: text.AlignCenter}, {Number: 3, Align: text.AlignLeft}}
|
||||
}
|
||||
|
||||
// returns a row for status type table based on the control summary
|
||||
func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) []string {
|
||||
func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary) table.Row {
|
||||
|
||||
// show only passed, failed and action required controls
|
||||
status := controlSummary.GetStatus()
|
||||
@@ -63,7 +63,7 @@ func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary, inf
|
||||
return nil
|
||||
}
|
||||
|
||||
rows := make([]string, 3)
|
||||
rows := make(table.Row, 3)
|
||||
|
||||
rows[0] = utils.GetStatusIcon(controlSummary.GetStatus().Status())
|
||||
|
||||
@@ -80,31 +80,26 @@ func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary, inf
|
||||
|
||||
}
|
||||
|
||||
func getCategoryTableWriter(writer io.Writer, headers []string, columnAligments []int) *tablewriter.Table {
|
||||
table := tablewriter.NewWriter(writer)
|
||||
table.SetHeader(headers)
|
||||
table.SetHeaderLine(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table.SetColumnAlignment(columnAligments)
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
var headerColors []tablewriter.Colors
|
||||
for range headers {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.FgHiYellowColor})
|
||||
}
|
||||
table.SetHeaderColor(headerColors...)
|
||||
return table
|
||||
func getCategoryTableWriter(writer io.Writer, headers table.Row, columnAlignments []table.ColumnConfig) table.Writer {
|
||||
tableWriter := table.NewWriter()
|
||||
tableWriter.SetOutputMirror(writer)
|
||||
tableWriter.AppendHeader(headers)
|
||||
tableWriter.Style().Options.SeparateHeader = true
|
||||
tableWriter.Style().Format.HeaderAlign = text.AlignLeft
|
||||
tableWriter.Style().Format.Header = text.FormatDefault
|
||||
tableWriter.SetColumnConfigs(columnAlignments)
|
||||
tableWriter.Style().Box = table.StyleBoxRounded
|
||||
return tableWriter
|
||||
}
|
||||
|
||||
func renderSingleCategory(writer io.Writer, categoryName string, table *tablewriter.Table, rows [][]string, infoToPrintInfo []utils.InfoStars) {
|
||||
func renderSingleCategory(writer io.Writer, categoryName string, tableWriter table.Writer, rows []table.Row, infoToPrintInfo []utils.InfoStars) {
|
||||
|
||||
cautils.InfoDisplay(writer, categoryName+"\n")
|
||||
|
||||
table.ClearRows()
|
||||
table.AppendBulk(rows)
|
||||
tableWriter.ResetRows()
|
||||
tableWriter.AppendRows(rows)
|
||||
|
||||
table.Render()
|
||||
tableWriter.Render()
|
||||
|
||||
if len(infoToPrintInfo) > 0 {
|
||||
printCategoryInfo(writer, infoToPrintInfo)
|
||||
|
||||
@@ -3,13 +3,13 @@ package configurationprinter
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -17,20 +17,20 @@ func TestInitCategoryTableData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
categoryType CategoryType
|
||||
expectedHeaders []string
|
||||
expectedAlignments []int
|
||||
expectedHeaders table.Row
|
||||
expectedAlignments []table.ColumnConfig
|
||||
}{
|
||||
{
|
||||
name: "Test1",
|
||||
categoryType: TypeCounting,
|
||||
expectedHeaders: []string{"Control name", "Resources", "View details"},
|
||||
expectedAlignments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT},
|
||||
expectedHeaders: table.Row{"Control name", "Resources", "View details"},
|
||||
expectedAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignLeft}, {Number: 2, Align: text.AlignCenter}, {Number: 3, Align: text.AlignLeft}},
|
||||
},
|
||||
{
|
||||
name: "Test2",
|
||||
categoryType: TypeStatus,
|
||||
expectedHeaders: []string{"", "Control name", "Docs"},
|
||||
expectedAlignments: []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER},
|
||||
expectedHeaders: table.Row{"", "Control name", "Docs"},
|
||||
expectedAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignCenter}, {Number: 2, Align: text.AlignLeft}, {Number: 3, Align: text.AlignCenter}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -42,8 +42,8 @@ func TestInitCategoryTableData(t *testing.T) {
|
||||
if len(alignments) != len(tt.expectedAlignments) {
|
||||
t.Errorf("initCategoryTableData() alignments = %v, want %v", alignments, tt.expectedAlignments)
|
||||
}
|
||||
assert.True(t, reflect.DeepEqual(headers, tt.expectedHeaders))
|
||||
assert.True(t, reflect.DeepEqual(alignments, tt.expectedAlignments))
|
||||
assert.Equal(t, headers, tt.expectedHeaders)
|
||||
assert.Equal(t, alignments, tt.expectedAlignments)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,52 +88,12 @@ func TestGetCategoryCountingTypeHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatusTypeAlignments(t *testing.T) {
|
||||
alignments := getStatusTypeAlignments()
|
||||
|
||||
if len(alignments) != 3 {
|
||||
t.Errorf("Expected 3 alignments, got %d", len(alignments))
|
||||
}
|
||||
|
||||
if alignments[0] != tablewriter.ALIGN_CENTER {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_CENTER, alignments[0])
|
||||
}
|
||||
|
||||
if alignments[1] != tablewriter.ALIGN_LEFT {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_LEFT, alignments[1])
|
||||
}
|
||||
|
||||
if alignments[2] != tablewriter.ALIGN_CENTER {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_CENTER, alignments[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCountingTypeAlignments(t *testing.T) {
|
||||
alignments := getCountingTypeAlignments()
|
||||
|
||||
if len(alignments) != 3 {
|
||||
t.Errorf("Expected 3 alignments, got %d", len(alignments))
|
||||
}
|
||||
|
||||
if alignments[0] != tablewriter.ALIGN_LEFT {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_LEFT, alignments[0])
|
||||
}
|
||||
|
||||
if alignments[1] != tablewriter.ALIGN_CENTER {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_CENTER, alignments[1])
|
||||
}
|
||||
|
||||
if alignments[2] != tablewriter.ALIGN_LEFT {
|
||||
t.Errorf("Expected %d, got %d", tablewriter.ALIGN_LEFT, alignments[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCategoryStatusRow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
controlSummary reportsummary.IControlSummary
|
||||
infoToPrintInfo []utils.InfoStars
|
||||
expectedRows []string
|
||||
expectedRows table.Row
|
||||
}{
|
||||
{
|
||||
name: "failed control",
|
||||
@@ -142,7 +102,7 @@ func TestGenerateCategoryStatusRow(t *testing.T) {
|
||||
Status: apis.StatusFailed,
|
||||
ControlID: "ctrlID",
|
||||
},
|
||||
expectedRows: []string{"❌", "test", "https://hub.armosec.io/docs/ctrlid"},
|
||||
expectedRows: table.Row{"❌", "test", "https://kubescape.io/docs/controls/ctrlid"},
|
||||
},
|
||||
{
|
||||
name: "skipped control",
|
||||
@@ -154,7 +114,7 @@ func TestGenerateCategoryStatusRow(t *testing.T) {
|
||||
},
|
||||
ControlID: "ctrlID",
|
||||
},
|
||||
expectedRows: []string{"⚠️", "test", "https://hub.armosec.io/docs/ctrlid"},
|
||||
expectedRows: table.Row{"⚠️", "test", "https://kubescape.io/docs/controls/ctrlid"},
|
||||
infoToPrintInfo: []utils.InfoStars{
|
||||
{
|
||||
Info: "testInfo",
|
||||
@@ -169,7 +129,7 @@ func TestGenerateCategoryStatusRow(t *testing.T) {
|
||||
Status: apis.StatusPassed,
|
||||
ControlID: "ctrlID",
|
||||
},
|
||||
expectedRows: []string{"✅", "test", "https://hub.armosec.io/docs/ctrlid"},
|
||||
expectedRows: table.Row{"✅", "test", "https://kubescape.io/docs/controls/ctrlid"},
|
||||
},
|
||||
{
|
||||
name: "big name",
|
||||
@@ -178,36 +138,36 @@ func TestGenerateCategoryStatusRow(t *testing.T) {
|
||||
Status: apis.StatusFailed,
|
||||
ControlID: "ctrlID",
|
||||
},
|
||||
expectedRows: []string{"❌", "testtesttesttesttesttesttesttesttesttesttesttestte...", "https://hub.armosec.io/docs/ctrlid"},
|
||||
expectedRows: table.Row{"❌", "testtesttesttesttesttesttesttesttesttesttesttestte...", "https://kubescape.io/docs/controls/ctrlid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
row := generateCategoryStatusRow(tt.controlSummary, tt.infoToPrintInfo)
|
||||
assert.True(t, reflect.DeepEqual(row, tt.expectedRows))
|
||||
row := generateCategoryStatusRow(tt.controlSummary)
|
||||
assert.Equal(t, tt.expectedRows, row)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCategoryTableWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers []string
|
||||
columnAligments []int
|
||||
want string
|
||||
name string
|
||||
headers table.Row
|
||||
columnAlignments []table.ColumnConfig
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Test1",
|
||||
headers: []string{"Control name", "Resources", "View details"},
|
||||
columnAligments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT},
|
||||
want: "┌──────────────┬───────────┬──────────────┐\n│ Control name │ Resources │ View details │\n├──────────────┼───────────┼──────────────┤\n└──────────────┴───────────┴──────────────┘\n",
|
||||
name: "Test1",
|
||||
headers: table.Row{"Control name", "Resources", "View details"},
|
||||
columnAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignLeft}, {Number: 2, Align: text.AlignCenter}, {Number: 3, Align: text.AlignLeft}},
|
||||
want: "╭──────────────┬───────────┬──────────────╮\n│ Control name │ Resources │ View details │\n├──────────────┼───────────┼──────────────┤\n╰──────────────┴───────────┴──────────────╯\n",
|
||||
},
|
||||
{
|
||||
name: "Test2",
|
||||
headers: []string{"", "Control name", "Docs"},
|
||||
columnAligments: []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER},
|
||||
want: "┌──┬──────────────┬──────┐\n│ │ Control name │ Docs │\n├──┼──────────────┼──────┤\n└──┴──────────────┴──────┘\n",
|
||||
name: "Test2",
|
||||
headers: table.Row{"", "Control name", "Docs"},
|
||||
columnAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignCenter}, {Number: 2, Align: text.AlignLeft}, {Number: 3, Align: text.AlignCenter}},
|
||||
want: "╭──┬──────────────┬──────╮\n│ │ Control name │ Docs │\n├──┼──────────────┼──────┤\n╰──┴──────────────┴──────╯\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -219,7 +179,7 @@ func TestGetCategoryTableWriter(t *testing.T) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tableWriter := getCategoryTableWriter(f, tt.headers, tt.columnAligments)
|
||||
tableWriter := getCategoryTableWriter(f, tt.headers, tt.columnAlignments)
|
||||
|
||||
// Redirect stderr to the temporary file
|
||||
oldStderr := os.Stderr
|
||||
@@ -245,61 +205,61 @@ func TestGetCategoryTableWriter(t *testing.T) {
|
||||
|
||||
func TestRenderSingleCategory(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
categoryName string
|
||||
rows [][]string
|
||||
infoToPrintInfo []utils.InfoStars
|
||||
headers []string
|
||||
columnAligments []int
|
||||
want string
|
||||
name string
|
||||
categoryName string
|
||||
rows []table.Row
|
||||
infoToPrintInfo []utils.InfoStars
|
||||
headers table.Row
|
||||
columnAlignments []table.ColumnConfig
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Test1",
|
||||
categoryName: "Resources",
|
||||
rows: [][]string{
|
||||
rows: []table.Row{
|
||||
{"Regular", "regular line", "1"},
|
||||
{"Thick", "particularly thick line", "2"},
|
||||
{"Double", "double line", "3"},
|
||||
},
|
||||
infoToPrintInfo: []utils.InfoStars{
|
||||
utils.InfoStars{
|
||||
{
|
||||
Stars: "1",
|
||||
Info: "Low severity",
|
||||
},
|
||||
utils.InfoStars{
|
||||
{
|
||||
Stars: "5",
|
||||
Info: "Critical severity",
|
||||
},
|
||||
},
|
||||
headers: []string{"Control name", "Resources", "View details"},
|
||||
columnAligments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT},
|
||||
want: "Resources\n┌──────────────┬─────────────────────────┬──────────────┐\n│ Control name │ Resources │ View details │\n├──────────────┼─────────────────────────┼──────────────┤\n│ Regular │ regular line │ 1 │\n│ Thick │ particularly thick line │ 2 │\n│ Double │ double line │ 3 │\n└──────────────┴─────────────────────────┴──────────────┘\n1 Low severity\n5 Critical severity\n\n",
|
||||
headers: table.Row{"Control name", "Resources", "View details"},
|
||||
columnAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignLeft}, {Number: 2, Align: text.AlignCenter}, {Number: 3, Align: text.AlignLeft}},
|
||||
want: "Resources\n╭──────────────┬─────────────────────────┬──────────────╮\n│ Control name │ Resources │ View details │\n├──────────────┼─────────────────────────┼──────────────┤\n│ Regular │ regular line │ 1 │\n│ Thick │ particularly thick line │ 2 │\n│ Double │ double line │ 3 │\n╰──────────────┴─────────────────────────┴──────────────╯\n1 Low severity\n5 Critical severity\n\n",
|
||||
},
|
||||
{
|
||||
name: "Test2",
|
||||
categoryName: "Control name",
|
||||
rows: [][]string{
|
||||
rows: []table.Row{
|
||||
{"Regular", "regular line", "1"},
|
||||
{"Thick", "particularly thick line", "2"},
|
||||
{"Double", "double line", "3"},
|
||||
},
|
||||
infoToPrintInfo: []utils.InfoStars{
|
||||
utils.InfoStars{
|
||||
{
|
||||
Stars: "1",
|
||||
Info: "Low severity",
|
||||
},
|
||||
utils.InfoStars{
|
||||
{
|
||||
Stars: "5",
|
||||
Info: "Critical severity",
|
||||
},
|
||||
utils.InfoStars{
|
||||
{
|
||||
Stars: "4",
|
||||
Info: "High severity",
|
||||
},
|
||||
},
|
||||
headers: []string{"Control name", "Resources", "View details"},
|
||||
columnAligments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT},
|
||||
want: "Control name\n┌──────────────┬─────────────────────────┬──────────────┐\n│ Control name │ Resources │ View details │\n├──────────────┼─────────────────────────┼──────────────┤\n│ Regular │ regular line │ 1 │\n│ Thick │ particularly thick line │ 2 │\n│ Double │ double line │ 3 │\n└──────────────┴─────────────────────────┴──────────────┘\n1 Low severity\n5 Critical severity\n4 High severity\n\n",
|
||||
headers: table.Row{"Control name", "Resources", "View details"},
|
||||
columnAlignments: []table.ColumnConfig{{Number: 1, Align: text.AlignLeft}, {Number: 2, Align: text.AlignCenter}, {Number: 3, Align: text.AlignLeft}},
|
||||
want: "Control name\n╭──────────────┬─────────────────────────┬──────────────╮\n│ Control name │ Resources │ View details │\n├──────────────┼─────────────────────────┼──────────────┤\n│ Regular │ regular line │ 1 │\n│ Thick │ particularly thick line │ 2 │\n│ Double │ double line │ 3 │\n╰──────────────┴─────────────────────────┴──────────────╯\n1 Low severity\n5 Critical severity\n4 High severity\n\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -311,7 +271,7 @@ func TestRenderSingleCategory(t *testing.T) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tableWriter := getCategoryTableWriter(f, tt.headers, tt.columnAligments)
|
||||
tableWriter := getCategoryTableWriter(f, tt.headers, tt.columnAlignments)
|
||||
|
||||
// Redirect stderr to the temporary file
|
||||
oldStderr := os.Stderr
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
@@ -17,11 +18,11 @@ func NewClusterPrinter() *ClusterPrinter {
|
||||
|
||||
var _ TablePrinter = &ClusterPrinter{}
|
||||
|
||||
func (cp *ClusterPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (cp *ClusterPrinter) PrintSummaryTable(_ io.Writer, _ *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
}
|
||||
|
||||
func (cp *ClusterPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (cp *ClusterPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
categoriesToCategoryControls := mapCategoryToSummary(summaryDetails.ListControls(), mapClusterControlsToCategories)
|
||||
|
||||
@@ -38,17 +39,17 @@ func (cp *ClusterPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails
|
||||
func (cp *ClusterPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) {
|
||||
sortControlSummaries(controlSummaries)
|
||||
|
||||
headers, columnAligments := initCategoryTableData(categoryType)
|
||||
headers, columnAlignments := initCategoryTableData(categoryType)
|
||||
|
||||
table := getCategoryTableWriter(writer, headers, columnAligments)
|
||||
tableWriter := getCategoryTableWriter(writer, headers, columnAlignments)
|
||||
|
||||
var rows [][]string
|
||||
var rows []table.Row
|
||||
for _, ctrls := range controlSummaries {
|
||||
var row []string
|
||||
var row table.Row
|
||||
if categoryType == TypeCounting {
|
||||
row = cp.generateCountingCategoryRow(ctrls)
|
||||
} else {
|
||||
row = generateCategoryStatusRow(ctrls, infoToPrintInfo)
|
||||
row = generateCategoryStatusRow(ctrls)
|
||||
}
|
||||
if len(row) > 0 {
|
||||
rows = append(rows, row)
|
||||
@@ -59,19 +60,19 @@ func (cp *ClusterPrinter) renderSingleCategoryTable(categoryName string, categor
|
||||
return
|
||||
}
|
||||
|
||||
renderSingleCategory(writer, categoryName, table, rows, infoToPrintInfo)
|
||||
renderSingleCategory(writer, categoryName, tableWriter, rows, infoToPrintInfo)
|
||||
|
||||
}
|
||||
|
||||
func (cp *ClusterPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary) []string {
|
||||
func (cp *ClusterPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary) table.Row {
|
||||
|
||||
row := make([]string, 3)
|
||||
row := make(table.Row, 3)
|
||||
|
||||
row[0] = controlSummary.GetName()
|
||||
|
||||
failedResources := controlSummary.NumberOfResources().Failed()
|
||||
if failedResources > 0 {
|
||||
row[1] = string(gchalk.WithYellow().Bold(fmt.Sprintf("%d", failedResources)))
|
||||
row[1] = gchalk.WithYellow().Bold(fmt.Sprintf("%d", failedResources))
|
||||
} else {
|
||||
row[1] = fmt.Sprintf("%d", failedResources)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"strconv"
|
||||
"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"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
type FrameworkPrinter struct {
|
||||
@@ -38,19 +38,21 @@ func (fp *FrameworkPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *
|
||||
// When scanning controls the framework list will be empty
|
||||
cautils.SimpleDisplay(writer, utils.FrameworksScoresToString(summaryDetails.ListFrameworks())+"\n")
|
||||
|
||||
controlCountersTable := tablewriter.NewWriter(writer)
|
||||
controlCountersTable := table.NewWriter()
|
||||
controlCountersTable.SetOutputMirror(writer)
|
||||
|
||||
controlCountersTable.SetColumnAlignment([]int{tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT})
|
||||
controlCountersTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
controlCountersTable.AppendBulk(ControlCountersForSummary(summaryDetails.NumberOfControls()))
|
||||
controlCountersTable.SetColumnConfigs([]table.ColumnConfig{{Number: 1, Align: text.AlignRight}, {Number: 2, Align: text.AlignLeft}})
|
||||
controlCountersTable.Style().Box = table.StyleBoxRounded
|
||||
controlCountersTable.AppendRows(ControlCountersForSummary(summaryDetails.NumberOfControls()))
|
||||
controlCountersTable.Render()
|
||||
|
||||
cautils.SimpleDisplay(writer, "\nFailed resources by severity:\n\n")
|
||||
|
||||
severityCountersTable := tablewriter.NewWriter(writer)
|
||||
severityCountersTable.SetColumnAlignment([]int{tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT})
|
||||
severityCountersTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
severityCountersTable.AppendBulk(renderSeverityCountersSummary(summaryDetails.GetResourcesSeverityCounters()))
|
||||
severityCountersTable := table.NewWriter()
|
||||
severityCountersTable.SetOutputMirror(writer)
|
||||
severityCountersTable.SetColumnConfigs([]table.ColumnConfig{{Number: 1, Align: text.AlignRight}, {Number: 2, Align: text.AlignLeft}})
|
||||
severityCountersTable.Style().Box = table.StyleBoxRounded
|
||||
severityCountersTable.AppendRows(renderSeverityCountersSummary(summaryDetails.GetResourcesSeverityCounters()))
|
||||
severityCountersTable.Render()
|
||||
|
||||
cautils.SimpleDisplay(writer, "\n")
|
||||
@@ -59,14 +61,15 @@ func (fp *FrameworkPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *
|
||||
cautils.SimpleDisplay(writer, "Run with '--verbose'/'-v' to see control failures for each resource.\n\n")
|
||||
}
|
||||
|
||||
summaryTable := tablewriter.NewWriter(writer)
|
||||
summaryTable := table.NewWriter()
|
||||
summaryTable.SetOutputMirror(writer)
|
||||
|
||||
summaryTable.SetAutoWrapText(false)
|
||||
summaryTable.SetHeaderLine(true)
|
||||
summaryTable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
summaryTable.SetAutoFormatHeaders(false)
|
||||
summaryTable.SetColumnAlignment(GetColumnsAlignments())
|
||||
summaryTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
summaryTable.Style().Options.SeparateHeader = true
|
||||
summaryTable.Style().Format.HeaderAlign = text.AlignLeft
|
||||
summaryTable.Style().Format.Header = text.FormatDefault
|
||||
summaryTable.Style().Format.Footer = text.FormatDefault
|
||||
summaryTable.SetColumnConfigs(GetColumnsAlignments())
|
||||
summaryTable.Style().Box = table.StyleBoxRounded
|
||||
|
||||
printAll := fp.getVerboseMode()
|
||||
if summaryDetails.NumberOfResources().Failed() == 0 {
|
||||
@@ -74,7 +77,7 @@ func (fp *FrameworkPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *
|
||||
printAll = true
|
||||
}
|
||||
|
||||
dataRows := [][]string{}
|
||||
var dataRows []table.Row
|
||||
|
||||
infoToPrintInfo := utils.MapInfoToPrintInfo(summaryDetails.Controls)
|
||||
for i := len(sortedControlIDs) - 1; i >= 0; i-- {
|
||||
@@ -88,28 +91,23 @@ func (fp *FrameworkPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *
|
||||
|
||||
short := utils.CheckShortTerminalWidth(dataRows, GetControlTableHeaders(false))
|
||||
if short {
|
||||
summaryTable.SetRowLine(true)
|
||||
summaryTable.Style().Options.SeparateRows = true
|
||||
dataRows = shortFormatRow(dataRows)
|
||||
} else {
|
||||
summaryTable.SetColumnAlignment(GetColumnsAlignments())
|
||||
summaryTable.SetColumnConfigs(GetColumnsAlignments())
|
||||
summaryTable.Style().Format.FooterAlign = text.AlignCenter
|
||||
}
|
||||
summaryTable.SetHeader(GetControlTableHeaders(short))
|
||||
summaryTable.SetFooter(GenerateFooter(summaryDetails, short))
|
||||
summaryTable.AppendHeader(GetControlTableHeaders(short))
|
||||
summaryTable.AppendFooter(GenerateFooter(summaryDetails, short))
|
||||
|
||||
var headerColors []tablewriter.Colors
|
||||
for range dataRows[0] {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor})
|
||||
}
|
||||
summaryTable.SetHeaderColor(headerColors...)
|
||||
|
||||
summaryTable.AppendBulk(dataRows)
|
||||
summaryTable.AppendRows(dataRows)
|
||||
summaryTable.Render()
|
||||
|
||||
utils.PrintInfo(writer, infoToPrintInfo)
|
||||
}
|
||||
|
||||
func shortFormatRow(dataRows [][]string) [][]string {
|
||||
rows := [][]string{}
|
||||
func shortFormatRow(dataRows []table.Row) []table.Row {
|
||||
rows := make([]table.Row, 0, len(dataRows))
|
||||
for _, dataRow := range dataRows {
|
||||
// Define the row content using a formatted string
|
||||
rowContent := fmt.Sprintf("Severity%s: %+v\nControl Name%s: %+v\nFailed Resources%s: %+v\nAll Resources%s: %+v\n%% Compliance-Score%s: %+v",
|
||||
@@ -125,22 +123,22 @@ func shortFormatRow(dataRows [][]string) [][]string {
|
||||
dataRow[summaryColumnComplianceScore])
|
||||
|
||||
// Append the formatted row content to the rows slice
|
||||
rows = append(rows, []string{rowContent})
|
||||
rows = append(rows, table.Row{rowContent})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (fp *FrameworkPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (fp *FrameworkPrinter) PrintCategoriesTables(_ io.Writer, _ *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
}
|
||||
|
||||
func renderSeverityCountersSummary(counters reportsummary.ISeverityCounters) [][]string {
|
||||
func renderSeverityCountersSummary(counters reportsummary.ISeverityCounters) []table.Row {
|
||||
|
||||
rows := [][]string{}
|
||||
rows = append(rows, []string{"Critical", utils.GetColorForVulnerabilitySeverity("Critical")(strconv.Itoa(counters.NumberOfCriticalSeverity()))})
|
||||
rows = append(rows, []string{"High", utils.GetColorForVulnerabilitySeverity("High")(strconv.Itoa(counters.NumberOfHighSeverity()))})
|
||||
rows = append(rows, []string{"Medium", utils.GetColorForVulnerabilitySeverity("Medium")(strconv.Itoa(counters.NumberOfMediumSeverity()))})
|
||||
rows = append(rows, []string{"Low", utils.GetColorForVulnerabilitySeverity("Low")(strconv.Itoa(counters.NumberOfLowSeverity()))})
|
||||
rows := make([]table.Row, 0, 4)
|
||||
rows = append(rows, table.Row{"Critical", utils.GetColorForVulnerabilitySeverity("Critical")(strconv.Itoa(counters.NumberOfCriticalSeverity()))})
|
||||
rows = append(rows, table.Row{"High", utils.GetColorForVulnerabilitySeverity("High")(strconv.Itoa(counters.NumberOfHighSeverity()))})
|
||||
rows = append(rows, table.Row{"Medium", utils.GetColorForVulnerabilitySeverity("Medium")(strconv.Itoa(counters.NumberOfMediumSeverity()))})
|
||||
rows = append(rows, table.Row{"Low", utils.GetColorForVulnerabilitySeverity("Low")(strconv.Itoa(counters.NumberOfLowSeverity()))})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -32,7 +33,7 @@ func (m *MockISeverityCounters) NumberOfLowSeverity() int {
|
||||
return m.LowCount
|
||||
}
|
||||
|
||||
func (m *MockISeverityCounters) Increase(severity string, amount int) {
|
||||
func (m *MockISeverityCounters) Increase(_ string, _ int) {
|
||||
}
|
||||
|
||||
func TestNewFrameworkPrinter(t *testing.T) {
|
||||
@@ -60,28 +61,28 @@ func TestGetVerboseMode(t *testing.T) {
|
||||
func TestShortRowFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows [][]string
|
||||
expectedRows [][]string
|
||||
rows []table.Row
|
||||
expectedRows []table.Row
|
||||
}{
|
||||
{
|
||||
name: "Test Empty rows",
|
||||
rows: [][]string{},
|
||||
expectedRows: [][]string{},
|
||||
rows: []table.Row{},
|
||||
expectedRows: []table.Row{},
|
||||
},
|
||||
{
|
||||
name: "Test Non empty row",
|
||||
rows: [][]string{
|
||||
rows: []table.Row{
|
||||
{"Medium", "Control 1", "2", "20", "0.8"},
|
||||
},
|
||||
expectedRows: [][]string{[]string{"Severity : Medium\nControl Name : Control 1\nFailed Resources : 2\nAll Resources : 20\n% Compliance-Score : 0.8"}},
|
||||
expectedRows: []table.Row{{"Severity : Medium\nControl Name : Control 1\nFailed Resources : 2\nAll Resources : 20\n% Compliance-Score : 0.8"}},
|
||||
},
|
||||
{
|
||||
name: "Test Non empty rows",
|
||||
rows: [][]string{
|
||||
rows: []table.Row{
|
||||
{"Medium", "Control 1", "2", "20", "0.8"},
|
||||
{"Low", "Control 2", "0", "30", "1.0"},
|
||||
},
|
||||
expectedRows: [][]string{[]string{"Severity : Medium\nControl Name : Control 1\nFailed Resources : 2\nAll Resources : 20\n% Compliance-Score : 0.8"}, []string{"Severity : Low\nControl Name : Control 2\nFailed Resources : 0\nAll Resources : 30\n% Compliance-Score : 1.0"}},
|
||||
expectedRows: []table.Row{{"Severity : Medium\nControl Name : Control 1\nFailed Resources : 2\nAll Resources : 20\n% Compliance-Score : 0.8"}, {"Severity : Low\nControl Name : Control 2\nFailed Resources : 0\nAll Resources : 30\n% Compliance-Score : 1.0"}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,12 +97,12 @@ func TestRenderSeverityCountersSummary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
counters MockISeverityCounters
|
||||
expected [][]string
|
||||
expected []table.Row
|
||||
}{
|
||||
{
|
||||
name: "All empty",
|
||||
counters: MockISeverityCounters{},
|
||||
expected: [][]string{[]string{"Critical", "0"}, []string{"High", "0"}, []string{"Medium", "0"}, []string{"Low", "0"}},
|
||||
expected: []table.Row{{"Critical", "0"}, {"High", "0"}, {"Medium", "0"}, {"Low", "0"}},
|
||||
},
|
||||
{
|
||||
name: "All different",
|
||||
@@ -111,7 +112,7 @@ func TestRenderSeverityCountersSummary(t *testing.T) {
|
||||
MediumCount: 27,
|
||||
LowCount: 37,
|
||||
},
|
||||
expected: [][]string{[]string{"Critical", "7"}, []string{"High", "17"}, []string{"Medium", "27"}, []string{"Low", "37"}},
|
||||
expected: []table.Row{{"Critical", "7"}, {"High", "17"}, {"Medium", "27"}, {"Low", "37"}},
|
||||
},
|
||||
{
|
||||
name: "All equal",
|
||||
@@ -121,7 +122,7 @@ func TestRenderSeverityCountersSummary(t *testing.T) {
|
||||
MediumCount: 7,
|
||||
LowCount: 7,
|
||||
},
|
||||
expected: [][]string{[]string{"Critical", "7"}, []string{"High", "7"}, []string{"Medium", "7"}, []string{"Low", "7"}},
|
||||
expected: []table.Row{{"Critical", "7"}, {"High", "7"}, {"Medium", "7"}, {"Low", "7"}},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
@@ -24,15 +25,15 @@ func NewRepoPrinter(inputPatterns []string) *RepoPrinter {
|
||||
|
||||
var _ TablePrinter = &RepoPrinter{}
|
||||
|
||||
func (rp *RepoPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (rp *RepoPrinter) PrintSummaryTable(_ io.Writer, _ *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
}
|
||||
|
||||
func (rp *RepoPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (rp *RepoPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
categoriesToCategoryControls := mapCategoryToSummary(summaryDetails.ListControls(), mapRepoControlsToCategories)
|
||||
|
||||
tableRended := false
|
||||
tableRendered := false
|
||||
for _, id := range repoCategoriesDisplayOrder {
|
||||
categoryControl, ok := categoriesToCategoryControls[id]
|
||||
if !ok {
|
||||
@@ -43,10 +44,10 @@ func (rp *RepoPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *r
|
||||
continue
|
||||
}
|
||||
|
||||
tableRended = tableRended || rp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries))
|
||||
tableRendered = tableRendered || rp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries))
|
||||
}
|
||||
|
||||
if !tableRended {
|
||||
if !tableRendered {
|
||||
fmt.Fprintln(writer, gchalk.WithGreen().Bold("All controls passed. No issues found"))
|
||||
}
|
||||
|
||||
@@ -55,21 +56,21 @@ func (rp *RepoPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *r
|
||||
func (rp *RepoPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) bool {
|
||||
sortControlSummaries(controlSummaries)
|
||||
|
||||
headers, columnAligments := initCategoryTableData(categoryType)
|
||||
headers, columnAlignments := initCategoryTableData(categoryType)
|
||||
|
||||
table := getCategoryTableWriter(writer, headers, columnAligments)
|
||||
tableWriter := getCategoryTableWriter(writer, headers, columnAlignments)
|
||||
|
||||
var rows [][]string
|
||||
var rows []table.Row
|
||||
for _, ctrls := range controlSummaries {
|
||||
if ctrls.NumberOfResources().Failed() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var row []string
|
||||
var row table.Row
|
||||
if categoryType == TypeCounting {
|
||||
row = rp.generateCountingCategoryRow(ctrls, rp.inputPatterns)
|
||||
} else {
|
||||
row = generateCategoryStatusRow(ctrls, infoToPrintInfo)
|
||||
row = generateCategoryStatusRow(ctrls)
|
||||
}
|
||||
if len(row) > 0 {
|
||||
rows = append(rows, row)
|
||||
@@ -80,18 +81,18 @@ func (rp *RepoPrinter) renderSingleCategoryTable(categoryName string, categoryTy
|
||||
return false
|
||||
}
|
||||
|
||||
renderSingleCategory(writer, categoryName, table, rows, infoToPrintInfo)
|
||||
renderSingleCategory(writer, categoryName, tableWriter, rows, infoToPrintInfo)
|
||||
return true
|
||||
}
|
||||
|
||||
func (rp *RepoPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary, inputPatterns []string) []string {
|
||||
rows := make([]string, 3)
|
||||
func (rp *RepoPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary, inputPatterns []string) table.Row {
|
||||
rows := make(table.Row, 3)
|
||||
|
||||
rows[0] = controlSummary.GetName()
|
||||
|
||||
failedResources := controlSummary.NumberOfResources().Failed()
|
||||
if failedResources > 0 {
|
||||
rows[1] = string(gchalk.WithYellow().Bold(fmt.Sprintf("%d", failedResources)))
|
||||
rows[1] = gchalk.WithYellow().Bold(fmt.Sprintf("%d", failedResources))
|
||||
} else {
|
||||
rows[1] = fmt.Sprintf("%d", failedResources)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,12 +22,12 @@ const (
|
||||
_summaryRowLen = iota
|
||||
)
|
||||
|
||||
func ControlCountersForSummary(counters reportsummary.ICounters) [][]string {
|
||||
rows := [][]string{}
|
||||
rows = append(rows, []string{"Controls", strconv.Itoa(counters.All())})
|
||||
rows = append(rows, []string{"Passed", strconv.Itoa(counters.Passed())})
|
||||
rows = append(rows, []string{"Failed", strconv.Itoa(counters.Failed())})
|
||||
rows = append(rows, []string{"Action Required", strconv.Itoa(counters.Skipped())})
|
||||
func ControlCountersForSummary(counters reportsummary.ICounters) []table.Row {
|
||||
rows := make([]table.Row, 0, 4)
|
||||
rows = append(rows, table.Row{"Controls", strconv.Itoa(counters.All())})
|
||||
rows = append(rows, table.Row{"Passed", strconv.Itoa(counters.Passed())})
|
||||
rows = append(rows, table.Row{"Failed", strconv.Itoa(counters.Failed())})
|
||||
rows = append(rows, table.Row{"Action Required", strconv.Itoa(counters.Skipped())})
|
||||
|
||||
return rows
|
||||
}
|
||||
@@ -35,13 +36,13 @@ func GetSeverityColumn(controlSummary reportsummary.IControlSummary) string {
|
||||
return utils.GetColor(apis.ControlSeverityToInt(controlSummary.GetScoreFactor()))(apis.ControlSeverityToString(controlSummary.GetScoreFactor()))
|
||||
}
|
||||
|
||||
func GetControlTableHeaders(short bool) []string {
|
||||
var headers []string
|
||||
func GetControlTableHeaders(short bool) table.Row {
|
||||
var headers table.Row
|
||||
if short {
|
||||
headers = make([]string, 1)
|
||||
headers = make(table.Row, 1)
|
||||
headers[0] = "Controls"
|
||||
} else {
|
||||
headers = make([]string, _summaryRowLen)
|
||||
headers = make(table.Row, _summaryRowLen)
|
||||
headers[summaryColumnName] = "Control name"
|
||||
headers[summaryColumnCounterFailed] = "Failed resources"
|
||||
headers[summaryColumnCounterAll] = "All Resources"
|
||||
@@ -51,22 +52,22 @@ func GetControlTableHeaders(short bool) []string {
|
||||
return headers
|
||||
}
|
||||
|
||||
func GetColumnsAlignments() []int {
|
||||
alignments := make([]int, _summaryRowLen)
|
||||
alignments[summaryColumnSeverity] = tablewriter.ALIGN_CENTER
|
||||
alignments[summaryColumnName] = tablewriter.ALIGN_LEFT
|
||||
alignments[summaryColumnCounterFailed] = tablewriter.ALIGN_CENTER
|
||||
alignments[summaryColumnCounterAll] = tablewriter.ALIGN_CENTER
|
||||
alignments[summaryColumnComplianceScore] = tablewriter.ALIGN_CENTER
|
||||
return alignments
|
||||
func GetColumnsAlignments() []table.ColumnConfig {
|
||||
return []table.ColumnConfig{
|
||||
{Number: summaryColumnSeverity + 1, Align: text.AlignCenter},
|
||||
{Number: summaryColumnName + 1, Align: text.AlignLeft},
|
||||
{Number: summaryColumnCounterFailed + 1, Align: text.AlignCenter},
|
||||
{Number: summaryColumnCounterAll + 1, Align: text.AlignCenter},
|
||||
{Number: summaryColumnComplianceScore + 1, Align: text.AlignCenter},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateRow(controlSummary reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars, verbose bool) []string {
|
||||
row := make([]string, _summaryRowLen)
|
||||
func GenerateRow(controlSummary reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars, verbose bool) table.Row {
|
||||
row := make(table.Row, _summaryRowLen)
|
||||
|
||||
// ignore passed results
|
||||
if !verbose && (controlSummary.GetStatus().IsPassed()) {
|
||||
return []string{}
|
||||
return table.Row{}
|
||||
}
|
||||
|
||||
row[summaryColumnSeverity] = GetSeverityColumn(controlSummary)
|
||||
@@ -98,14 +99,14 @@ func GetInfoColumn(controlSummary reportsummary.IControlSummary, infoToPrintInfo
|
||||
return ""
|
||||
}
|
||||
|
||||
func GenerateFooter(summaryDetails *reportsummary.SummaryDetails, short bool) []string {
|
||||
var row []string
|
||||
func GenerateFooter(summaryDetails *reportsummary.SummaryDetails, short bool) table.Row {
|
||||
var row table.Row
|
||||
if short {
|
||||
row = make([]string, 1)
|
||||
row = make(table.Row, 1)
|
||||
row[0] = fmt.Sprintf("Resource Summary"+strings.Repeat(" ", 0)+"\n\nFailed Resources"+strings.Repeat(" ", 1)+": %d\nAll Resources"+strings.Repeat(" ", 4)+": %d\n%% Compliance-Score"+strings.Repeat(" ", 4)+": %.2f%%", summaryDetails.NumberOfResources().Failed(), summaryDetails.NumberOfResources().All(), summaryDetails.ComplianceScore)
|
||||
} else {
|
||||
// Severity | Control name | failed resources | all resources | % success
|
||||
row = make([]string, _summaryRowLen)
|
||||
row = make(table.Row, _summaryRowLen)
|
||||
row[summaryColumnName] = "Resource Summary"
|
||||
row[summaryColumnCounterFailed] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().Failed())
|
||||
row[summaryColumnCounterAll] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().All())
|
||||
|
||||
@@ -542,14 +542,14 @@ func TestGetDocsForControl(t *testing.T) {
|
||||
controlSummary: &reportsummary.ControlSummary{
|
||||
ControlID: "ctrlID1",
|
||||
},
|
||||
expectedDocsLink: "https://hub.armosec.io/docs/ctrlid1",
|
||||
expectedDocsLink: "https://kubescape.io/docs/controls/ctrlid1",
|
||||
},
|
||||
{
|
||||
name: "control with lowercase ID",
|
||||
controlSummary: &reportsummary.ControlSummary{
|
||||
ControlID: "ctrlid1",
|
||||
},
|
||||
expectedDocsLink: "https://hub.armosec.io/docs/ctrlid1",
|
||||
expectedDocsLink: "https://kubescape.io/docs/controls/ctrlid1",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package configurationprinter
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
)
|
||||
@@ -16,11 +17,11 @@ func NewWorkloadPrinter() *WorkloadPrinter {
|
||||
return &WorkloadPrinter{}
|
||||
}
|
||||
|
||||
func (wp *WorkloadPrinter) PrintSummaryTable(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (wp *WorkloadPrinter) PrintSummaryTable(_ io.Writer, _ *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
}
|
||||
|
||||
func (wp *WorkloadPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) {
|
||||
func (wp *WorkloadPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *reportsummary.SummaryDetails, _ [][]string) {
|
||||
|
||||
categoriesToCategoryControls := mapCategoryToSummary(summaryDetails.ListControls(), mapWorkloadControlsToCategories)
|
||||
|
||||
@@ -30,21 +31,20 @@ func (wp *WorkloadPrinter) PrintCategoriesTables(writer io.Writer, summaryDetail
|
||||
continue
|
||||
}
|
||||
|
||||
wp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries))
|
||||
wp.renderSingleCategoryTable(categoryControl.CategoryName, writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries))
|
||||
}
|
||||
}
|
||||
|
||||
func (wp *WorkloadPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) {
|
||||
func (wp *WorkloadPrinter) renderSingleCategoryTable(categoryName string, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) {
|
||||
sortControlSummaries(controlSummaries)
|
||||
|
||||
headers, columnAligments := wp.initCategoryTableData()
|
||||
headers, columnAlignments := wp.initCategoryTableData()
|
||||
|
||||
table := getCategoryTableWriter(writer, headers, columnAligments)
|
||||
tableWriter := getCategoryTableWriter(writer, headers, columnAlignments)
|
||||
|
||||
var rows [][]string
|
||||
var rows []table.Row
|
||||
for _, ctrls := range controlSummaries {
|
||||
var row []string
|
||||
row = generateCategoryStatusRow(ctrls, infoToPrintInfo)
|
||||
row := generateCategoryStatusRow(ctrls)
|
||||
if len(row) > 0 {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
@@ -54,9 +54,9 @@ func (wp *WorkloadPrinter) renderSingleCategoryTable(categoryName string, catego
|
||||
return
|
||||
}
|
||||
|
||||
renderSingleCategory(writer, categoryName, table, rows, infoToPrintInfo)
|
||||
renderSingleCategory(writer, categoryName, tableWriter, rows, infoToPrintInfo)
|
||||
}
|
||||
|
||||
func (wp *WorkloadPrinter) initCategoryTableData() ([]string, []int) {
|
||||
func (wp *WorkloadPrinter) initCategoryTableData() (table.Row, []table.ColumnConfig) {
|
||||
return getCategoryStatusTypeHeaders(), getStatusTypeAlignments()
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@ package configurationprinter
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWorkloadScan_InitCategoryTableData(t *testing.T) {
|
||||
|
||||
expectedHeader := []string{"", "Control name", "Docs"}
|
||||
expectedAlign := []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER}
|
||||
expectedAlign := []table.ColumnConfig{{Number: 1, Align: text.AlignCenter}, {Number: 2, Align: text.AlignLeft}, {Number: 3, Align: text.AlignCenter}}
|
||||
|
||||
workloadPrinter := NewWorkloadPrinter()
|
||||
|
||||
headers, columnAligments := workloadPrinter.initCategoryTableData()
|
||||
headers, columnAlignments := workloadPrinter.initCategoryTableData()
|
||||
|
||||
for i := range headers {
|
||||
if headers[i] != expectedHeader[i] {
|
||||
@@ -21,10 +23,8 @@ func TestWorkloadScan_InitCategoryTableData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for i := range columnAligments {
|
||||
if columnAligments[i] != expectedAlign[i] {
|
||||
t.Errorf("Expected column alignment %d, got %d", expectedAlign[i], columnAligments[i])
|
||||
}
|
||||
for i := range columnAlignments {
|
||||
assert.Equal(t, expectedAlign[i], columnAlignments[i])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestPrintImageScanningTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "┌──────────┬───────────────┬───────────┬─────────┬──────────┐\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ │\n│ Medium │ CVE-2020-0003 │ package3 │ 1.0.0 │ │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n└──────────┴───────────────┴───────────┴─────────┴──────────┘\n",
|
||||
want: "╭──────────┬───────────────┬───────────┬─────────┬──────────╮\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ │\n│ Medium │ CVE-2020-0003 │ package3 │ 1.0.0 │ │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n╰──────────┴───────────────┴───────────┴─────────┴──────────╯\n",
|
||||
},
|
||||
{
|
||||
name: "check fixed CVEs show versions",
|
||||
@@ -65,7 +65,7 @@ func TestPrintImageScanningTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "┌──────────┬───────────────┬───────────┬─────────┬──────────┐\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ v1,v2 │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n└──────────┴───────────────┴───────────┴─────────┴──────────┘\n",
|
||||
want: "╭──────────┬───────────────┬───────────┬─────────┬──────────╮\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ v1,v2 │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n╰──────────┴───────────────┴───────────┴─────────┴──────────╯\n",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -6,33 +6,28 @@ import (
|
||||
"strings"
|
||||
|
||||
v5 "github.com/anchore/grype/grype/db/v5"
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
func renderTable(writer io.Writer, headers []string, columnAlignments []int, rows [][]string) {
|
||||
table := tablewriter.NewWriter(writer)
|
||||
table.SetHeader(headers)
|
||||
table.SetHeaderLine(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table.SetColumnAlignment(columnAlignments)
|
||||
table.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
func renderTable(writer io.Writer, headers table.Row, columnAlignments []table.ColumnConfig, rows []table.Row) {
|
||||
tableWriter := table.NewWriter()
|
||||
tableWriter.SetOutputMirror(writer)
|
||||
tableWriter.AppendHeader(headers)
|
||||
tableWriter.Style().Options.SeparateHeader = true
|
||||
tableWriter.Style().Format.HeaderAlign = text.AlignLeft
|
||||
tableWriter.Style().Format.Header = text.FormatDefault
|
||||
tableWriter.SetColumnConfigs(columnAlignments)
|
||||
tableWriter.Style().Box = table.StyleBoxRounded
|
||||
|
||||
var headerColors []tablewriter.Colors
|
||||
for range rows[0] {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor})
|
||||
}
|
||||
table.SetHeaderColor(headerColors...)
|
||||
tableWriter.AppendRows(rows)
|
||||
|
||||
table.AppendBulk(rows)
|
||||
|
||||
table.Render()
|
||||
tableWriter.Render()
|
||||
}
|
||||
|
||||
func generateRows(summary ImageScanSummary) [][]string {
|
||||
rows := make([][]string, 0, len(summary.CVEs))
|
||||
func generateRows(summary ImageScanSummary) []table.Row {
|
||||
rows := make([]table.Row, 0, len(summary.CVEs))
|
||||
|
||||
// sort CVEs by severity
|
||||
sort.Slice(summary.CVEs, func(i, j int) bool {
|
||||
@@ -46,8 +41,8 @@ func generateRows(summary ImageScanSummary) [][]string {
|
||||
return rows
|
||||
}
|
||||
|
||||
func generateRow(cve CVE) []string {
|
||||
row := make([]string, 5)
|
||||
func generateRow(cve CVE) table.Row {
|
||||
row := make(table.Row, 5)
|
||||
row[imageColumnSeverity] = utils.GetColorForVulnerabilitySeverity(cve.Severity)(cve.Severity)
|
||||
row[imageColumnName] = cve.ID
|
||||
row[imageColumnComponent] = cve.Package
|
||||
@@ -59,13 +54,15 @@ func generateRow(cve CVE) []string {
|
||||
// if the CVE is not fixed, show the state
|
||||
} else if cve.FixedState == string(v5.WontFixState) {
|
||||
row[imageColumnFixedIn] = cve.FixedState
|
||||
} else {
|
||||
row[imageColumnFixedIn] = ""
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func getImageScanningHeaders() []string {
|
||||
headers := make([]string, 5)
|
||||
func getImageScanningHeaders() table.Row {
|
||||
headers := make(table.Row, 5)
|
||||
headers[imageColumnSeverity] = "Severity"
|
||||
headers[imageColumnName] = "Vulnerability"
|
||||
headers[imageColumnComponent] = "Component"
|
||||
@@ -74,6 +71,12 @@ func getImageScanningHeaders() []string {
|
||||
return headers
|
||||
}
|
||||
|
||||
func getImageScanningColumnsAlignments() []int {
|
||||
return []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}
|
||||
func getImageScanningColumnsAlignments() []table.ColumnConfig {
|
||||
return []table.ColumnConfig{
|
||||
{Number: 1, Align: text.AlignCenter},
|
||||
{Number: 2, Align: text.AlignLeft},
|
||||
{Number: 3, Align: text.AlignLeft},
|
||||
{Number: 4, Align: text.AlignLeft},
|
||||
{Number: 5, Align: text.AlignLeft},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
v5 "github.com/anchore/grype/grype/db/v5"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -46,7 +45,7 @@ func TestRenderTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "┌──────────┬───────────────┬───────────┬─────────┬──────────┐\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ │\n│ Medium │ CVE-2020-0003 │ package3 │ 1.0.0 │ │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n└──────────┴───────────────┴───────────┴─────────┴──────────┘\n",
|
||||
want: "╭──────────┬───────────────┬───────────┬─────────┬──────────╮\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ │\n│ Medium │ CVE-2020-0003 │ package3 │ 1.0.0 │ │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n╰──────────┴───────────────┴───────────┴─────────┴──────────╯\n",
|
||||
},
|
||||
{
|
||||
name: "check fixed CVEs show versions",
|
||||
@@ -69,7 +68,7 @@ func TestRenderTable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "┌──────────┬───────────────┬───────────┬─────────┬──────────┐\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ v1,v2 │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n└──────────┴───────────────┴───────────┴─────────┴──────────┘\n",
|
||||
want: "╭──────────┬───────────────┬───────────┬─────────┬──────────╮\n│ Severity │ Vulnerability │ Component │ Version │ Fixed in │\n├──────────┼───────────────┼───────────┼─────────┼──────────┤\n│ High │ CVE-2020-0002 │ package2 │ 1.0.0 │ v1,v2 │\n│ Low │ CVE-2020-0001 │ package1 │ 1.0.0 │ │\n╰──────────┴───────────────┴───────────┴─────────┴──────────╯\n",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -247,15 +246,3 @@ func TestGetImageScanningHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetImageScanningColumnsAlignments(t *testing.T) {
|
||||
alignments := getImageScanningColumnsAlignments()
|
||||
|
||||
expectedAlignments := []int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}
|
||||
|
||||
for i := range alignments {
|
||||
if alignments[i] != expectedAlignments[i] {
|
||||
t.Errorf("expected %d, got %d", expectedAlignments[i], alignments[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/enescakir/emoji"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
@@ -138,19 +139,19 @@ func GetStatusIcon(status apis.ScanningStatus) string {
|
||||
}
|
||||
}
|
||||
|
||||
func CheckShortTerminalWidth(rows [][]string, headers []string) bool {
|
||||
func CheckShortTerminalWidth(rows []table.Row, headers table.Row) bool {
|
||||
maxWidth := 0
|
||||
for _, row := range rows {
|
||||
rowWidth := 0
|
||||
for idx, cell := range row {
|
||||
cellLen := len(cell)
|
||||
cellLen := len(cell.(string))
|
||||
if cellLen > 50 { // Take only 50 characters of each sentence for counting size
|
||||
cellLen = 50
|
||||
}
|
||||
if cellLen > len(headers[idx]) {
|
||||
if cellLen > len(headers[idx].(string)) {
|
||||
rowWidth += cellLen
|
||||
} else {
|
||||
rowWidth += len(headers[idx])
|
||||
rowWidth += len(headers[idx].(string))
|
||||
}
|
||||
rowWidth += 2
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
@@ -59,9 +58,6 @@ func (pp *PrometheusPrinter) generatePrometheusFormat(
|
||||
return m
|
||||
}
|
||||
|
||||
func (pp *PrometheusPrinter) PrintImageScan(context.Context, *models.PresenterConfig) {
|
||||
}
|
||||
|
||||
func (pp *PrometheusPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData) {
|
||||
if opaSessionObj == nil {
|
||||
logger.L().Ctx(ctx).Error("failed to print results, missing data")
|
||||
|
||||
@@ -2,16 +2,18 @@ package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jwalton/gchalk"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,61 +45,46 @@ func (prettyPrinter *PrettyPrinter) resourceTable(opaSessionObj *cautils.OPASess
|
||||
if resource.GetNamespace() != "" {
|
||||
fmt.Fprintf(prettyPrinter.writer, "Namespace: %s\n", resource.GetNamespace())
|
||||
}
|
||||
fmt.Fprintf(prettyPrinter.writer, "\n"+prettyprinter.ControlCountersForResource(result.ListControlsIDs(nil))+"\n\n")
|
||||
fmt.Fprintf(prettyPrinter.writer, "\n%s\n\n", prettyprinter.ControlCountersForResource(result.ListControlsIDs(nil)))
|
||||
|
||||
summaryTable := tablewriter.NewWriter(prettyPrinter.writer)
|
||||
summaryTable := table.NewWriter()
|
||||
summaryTable.SetOutputMirror(prettyPrinter.writer)
|
||||
|
||||
summaryTable.SetAutoWrapText(true)
|
||||
summaryTable.SetAutoMergeCells(true)
|
||||
summaryTable.SetHeaderLine(true)
|
||||
summaryTable.SetRowLine(true)
|
||||
summaryTable.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
summaryTable.SetAutoFormatHeaders(false)
|
||||
summaryTable.SetUnicodeHVC(tablewriter.Regular, tablewriter.Regular, gchalk.Ansi256(238))
|
||||
summaryTable.Style().Options.SeparateHeader = true
|
||||
summaryTable.Style().Options.SeparateRows = true
|
||||
summaryTable.Style().Format.HeaderAlign = text.AlignLeft
|
||||
summaryTable.Style().Format.Header = text.FormatDefault
|
||||
summaryTable.Style().Box = table.StyleBoxRounded
|
||||
|
||||
resourceRows := [][]string{}
|
||||
if raw := generateResourceRows(result.ListControls(), &opaSessionObj.Report.SummaryDetails); len(raw) > 0 {
|
||||
resourceRows = append(resourceRows, raw...)
|
||||
}
|
||||
resourceRows := generateResourceRows(result.ListControls(), &opaSessionObj.Report.SummaryDetails, resource)
|
||||
|
||||
short := utils.CheckShortTerminalWidth(resourceRows, generateResourceHeader(false))
|
||||
if short {
|
||||
summaryTable.SetAutoWrapText(false)
|
||||
summaryTable.SetAutoMergeCells(false)
|
||||
resourceRows = shortFormatResource(resourceRows)
|
||||
}
|
||||
summaryTable.SetHeader(generateResourceHeader(short))
|
||||
summaryTable.AppendHeader(generateResourceHeader(short))
|
||||
|
||||
var headerColors []tablewriter.Colors
|
||||
for range resourceRows[0] {
|
||||
headerColors = append(headerColors, tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor})
|
||||
}
|
||||
summaryTable.SetHeaderColor(headerColors...)
|
||||
|
||||
data := Matrix{}
|
||||
data = append(data, resourceRows...)
|
||||
// For control scan framework will be nil
|
||||
|
||||
sort.Sort(data)
|
||||
summaryTable.AppendBulk(data)
|
||||
summaryTable.AppendRows(resourceRows)
|
||||
|
||||
summaryTable.Render()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateResourceRows(controls []resourcesresults.ResourceAssociatedControl, summaryDetails *reportsummary.SummaryDetails) [][]string {
|
||||
rows := [][]string{}
|
||||
func generateResourceRows(controls []resourcesresults.ResourceAssociatedControl, summaryDetails *reportsummary.SummaryDetails, resource workloadinterface.IMetadata) []table.Row {
|
||||
var rows []table.Row
|
||||
|
||||
for i := range controls {
|
||||
row := make([]string, _resourceRowLen)
|
||||
row := make(table.Row, _resourceRowLen)
|
||||
|
||||
if !controls[i].GetStatus(nil).IsFailed() {
|
||||
continue
|
||||
}
|
||||
|
||||
row[resourceColumnURL] = cautils.GetControlLink(controls[i].GetID())
|
||||
row[resourceColumnPath] = strings.Join(AssistedRemediationPathsToString(&controls[i]), "\n")
|
||||
paths := AssistedRemediationPathsToString(&controls[i])
|
||||
addContainerNameToAssistedRemediation(resource, &paths)
|
||||
row[resourceColumnPath] = strings.Join(paths, "\n")
|
||||
row[resourceColumnName] = controls[i].GetName()
|
||||
|
||||
if c := summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controls[i].GetID()); c != nil {
|
||||
@@ -110,22 +97,32 @@ func generateResourceRows(controls []resourcesresults.ResourceAssociatedControl,
|
||||
return rows
|
||||
}
|
||||
|
||||
func generateResourceHeader(short bool) []string {
|
||||
headers := make([]string, 0)
|
||||
|
||||
if short {
|
||||
headers = append(headers, "Resources")
|
||||
} else {
|
||||
headers = append(headers, []string{"Severity", "Control name", "Docs", "Assisted remediation"}...)
|
||||
func addContainerNameToAssistedRemediation(resource workloadinterface.IMetadata, paths *[]string) {
|
||||
for i := range *paths {
|
||||
re := regexp.MustCompile(`spec\.containers\[(\d+)]`)
|
||||
match := re.FindStringSubmatch((*paths)[i])
|
||||
if len(match) == 2 {
|
||||
index, _ := strconv.Atoi(match[1])
|
||||
wl := workloadinterface.NewWorkloadObj(resource.GetObject())
|
||||
containers, _ := wl.GetContainers()
|
||||
containerName := containers[index].Name
|
||||
(*paths)[i] = (*paths)[i] + " (" + containerName + ")"
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
func shortFormatResource(resourceRows [][]string) [][]string {
|
||||
rows := [][]string{}
|
||||
for _, resourceRow := range resourceRows {
|
||||
rows = append(rows, []string{fmt.Sprintf("Severity"+strings.Repeat(" ", 13)+": %+v\nControl Name"+strings.Repeat(" ", 9)+": %+v\nDocs"+strings.Repeat(" ", 17)+": %+v\nAssisted Remediation"+strings.Repeat(" ", 1)+": %+v", resourceRow[resourceColumnSeverity], resourceRow[resourceColumnName], resourceRow[resourceColumnURL], strings.Replace(resourceRow[resourceColumnPath], "\n", "\n"+strings.Repeat(" ", 23), -1))})
|
||||
func generateResourceHeader(short bool) table.Row {
|
||||
if short {
|
||||
return table.Row{"Resources"}
|
||||
} else {
|
||||
return table.Row{"Severity", "Control name", "Docs", "Assisted remediation"}
|
||||
}
|
||||
}
|
||||
|
||||
func shortFormatResource(resourceRows []table.Row) []table.Row {
|
||||
rows := make([]table.Row, len(resourceRows))
|
||||
for i, resourceRow := range resourceRows {
|
||||
rows[i] = table.Row{fmt.Sprintf("Severity"+strings.Repeat(" ", 13)+": %+v\nControl Name"+strings.Repeat(" ", 9)+": %+v\nDocs"+strings.Repeat(" ", 17)+": %+v\nAssisted Remediation"+strings.Repeat(" ", 1)+": %+v", resourceRow[resourceColumnSeverity], resourceRow[resourceColumnName], resourceRow[resourceColumnURL], strings.Replace(resourceRow[resourceColumnPath].(string), "\n", "\n"+strings.Repeat(" ", 23), -1))}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/armoapi-go/armotypes"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
@@ -326,15 +328,15 @@ func TestFailedPathsToString(t *testing.T) {
|
||||
|
||||
func TestShortFormatResource(t *testing.T) {
|
||||
// Create a test case with an empty resourceRows slice
|
||||
emptyResourceRows := [][]string{}
|
||||
emptyResourceRows := []table.Row{}
|
||||
|
||||
// Create a test case with a single resource row
|
||||
singleResourceRow := [][]string{
|
||||
singleResourceRow := []table.Row{
|
||||
{"High", "Control1", "https://example.com/doc1", "Path1"},
|
||||
}
|
||||
|
||||
// Create a test case with multiple resource rows
|
||||
multipleResourceRows := [][]string{
|
||||
multipleResourceRows := []table.Row{
|
||||
{"Medium", "Control2", "https://example.com/doc2", "Path2"},
|
||||
{"Low", "Control3", "https://example.com/doc3", "Path3"},
|
||||
}
|
||||
@@ -343,11 +345,11 @@ func TestShortFormatResource(t *testing.T) {
|
||||
assert.Empty(t, actualRows)
|
||||
|
||||
actualRows = shortFormatResource(singleResourceRow)
|
||||
expectedRows := [][]string{{"Severity : High\nControl Name : Control1\nDocs : https://example.com/doc1\nAssisted Remediation : Path1"}}
|
||||
expectedRows := []table.Row{{"Severity : High\nControl Name : Control1\nDocs : https://example.com/doc1\nAssisted Remediation : Path1"}}
|
||||
assert.Equal(t, expectedRows, actualRows)
|
||||
|
||||
actualRows = shortFormatResource(multipleResourceRows)
|
||||
expectedRows = [][]string{{"Severity : Medium\nControl Name : Control2\nDocs : https://example.com/doc2\nAssisted Remediation : Path2"},
|
||||
expectedRows = []table.Row{{"Severity : Medium\nControl Name : Control2\nDocs : https://example.com/doc2\nAssisted Remediation : Path2"},
|
||||
{"Severity : Low\nControl Name : Control3\nDocs : https://example.com/doc3\nAssisted Remediation : Path3"}}
|
||||
assert.Equal(t, expectedRows, actualRows)
|
||||
}
|
||||
@@ -355,33 +357,47 @@ func TestShortFormatResource(t *testing.T) {
|
||||
func TestGenerateResourceHeader(t *testing.T) {
|
||||
// Test case 1: Short headers
|
||||
shortHeaders := generateResourceHeader(true)
|
||||
expectedShortHeaders := []string{"Resources"}
|
||||
expectedShortHeaders := table.Row{"Resources"}
|
||||
assert.Equal(t, expectedShortHeaders, shortHeaders)
|
||||
|
||||
// Test case 2: Full headers
|
||||
fullHeaders := generateResourceHeader(false)
|
||||
expectedFullHeaders := []string{"Severity", "Control name", "Docs", "Assisted remediation"}
|
||||
expectedFullHeaders := table.Row{"Severity", "Control name", "Docs", "Assisted remediation"}
|
||||
assert.Equal(t, expectedFullHeaders, fullHeaders)
|
||||
}
|
||||
|
||||
func TestGenerateResourceRows_Loop(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
summaryDetails reportsummary.SummaryDetails
|
||||
controls []resourcesresults.ResourceAssociatedControl
|
||||
expectedLen int
|
||||
name string
|
||||
summaryDetails reportsummary.SummaryDetails
|
||||
controls []resourcesresults.ResourceAssociatedControl
|
||||
resource workloadinterface.IMetadata
|
||||
expectedLen int
|
||||
expectedContainerName string
|
||||
}{
|
||||
{
|
||||
name: "Empty controls",
|
||||
summaryDetails: reportsummary.SummaryDetails{},
|
||||
controls: []resourcesresults.ResourceAssociatedControl{},
|
||||
expectedLen: 0,
|
||||
resource: workloadinterface.NewWorkloadObj(map[string]interface{}{
|
||||
"kind": "Pod",
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "alpine-container",
|
||||
"image": "alpine:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedLen: 0,
|
||||
expectedContainerName: "",
|
||||
},
|
||||
{
|
||||
name: "2 Failed Controls",
|
||||
summaryDetails: reportsummary.SummaryDetails{},
|
||||
controls: []resourcesresults.ResourceAssociatedControl{
|
||||
resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "control-1",
|
||||
Name: "Control 1",
|
||||
Status: apis.StatusInfo{},
|
||||
@@ -393,16 +409,16 @@ func TestGenerateResourceRows_Loop(t *testing.T) {
|
||||
|
||||
Paths: []armotypes.PosturePaths{
|
||||
{
|
||||
FailedPath: "some-path1",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsNonRoot=true",
|
||||
},
|
||||
{
|
||||
FailedPath: "random-path1",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsGroup=1000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "control-2",
|
||||
Name: "Control 2",
|
||||
Status: apis.StatusInfo{},
|
||||
@@ -413,23 +429,35 @@ func TestGenerateResourceRows_Loop(t *testing.T) {
|
||||
SubStatus: "configuration",
|
||||
Paths: []armotypes.PosturePaths{
|
||||
{
|
||||
FailedPath: "some-path2",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsNonRoot=true",
|
||||
},
|
||||
{
|
||||
FailedPath: "random-path2",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsGroup=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLen: 2,
|
||||
resource: workloadinterface.NewWorkloadObj(map[string]interface{}{
|
||||
"kind": "Pod",
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "alpine-container",
|
||||
"image": "alpine:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedLen: 2,
|
||||
expectedContainerName: "alpine-container",
|
||||
},
|
||||
{
|
||||
name: "One failed control",
|
||||
summaryDetails: reportsummary.SummaryDetails{},
|
||||
controls: []resourcesresults.ResourceAssociatedControl{
|
||||
resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "control-1",
|
||||
Name: "Control 1",
|
||||
Status: apis.StatusInfo{},
|
||||
@@ -441,16 +469,16 @@ func TestGenerateResourceRows_Loop(t *testing.T) {
|
||||
|
||||
Paths: []armotypes.PosturePaths{
|
||||
{
|
||||
FailedPath: "some-path1",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsNonRoot=true",
|
||||
},
|
||||
{
|
||||
FailedPath: "random-path1",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsGroup=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resourcesresults.ResourceAssociatedControl{
|
||||
{
|
||||
ControlID: "control-2",
|
||||
Name: "Control 2",
|
||||
Status: apis.StatusInfo{},
|
||||
@@ -461,24 +489,42 @@ func TestGenerateResourceRows_Loop(t *testing.T) {
|
||||
SubStatus: "configuration",
|
||||
Paths: []armotypes.PosturePaths{
|
||||
{
|
||||
FailedPath: "some-path2",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsNonRoot=true",
|
||||
},
|
||||
{
|
||||
FailedPath: "random-path2",
|
||||
FailedPath: "spec.template.spec.containers[0].securityContext.runAsGroup=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLen: 1,
|
||||
resource: workloadinterface.NewWorkloadObj(map[string]interface{}{
|
||||
"kind": "Pod",
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "nginx-container",
|
||||
"image": "nginx:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedLen: 1,
|
||||
expectedContainerName: "nginx-container",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rows := generateResourceRows(tt.controls, &tt.summaryDetails)
|
||||
rows := generateResourceRows(tt.controls, &tt.summaryDetails, tt.resource)
|
||||
assert.Equal(t, tt.expectedLen, len(rows))
|
||||
//remediation is the last column of the first row
|
||||
if len(rows) != 0 {
|
||||
remediation := rows[0][3]
|
||||
assert.Contains(t, remediation, tt.expectedContainerName)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
grypesarif "github.com/anchore/grype/grype/presenter/sarif"
|
||||
"github.com/kubescape/go-logger"
|
||||
"github.com/kubescape/go-logger/helpers"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
@@ -115,12 +116,14 @@ func (sp *SARIFPrinter) addResult(scanRun *sarif.Run, ctl reportsummary.IControl
|
||||
})
|
||||
}
|
||||
|
||||
func (sp *SARIFPrinter) printImageScan(ctx context.Context, scanResults *models.PresenterConfig) error {
|
||||
if scanResults == nil {
|
||||
return fmt.Errorf("no no image vulnerability data provided")
|
||||
func (sp *SARIFPrinter) printImageScan(ctx context.Context, scanResults cautils.ImageScanData) error {
|
||||
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 fmt.Errorf("failed to create document: %w", err)
|
||||
}
|
||||
|
||||
pres := presenter.GetPresenter(printer.SARIFFormat, "", false, *scanResults)
|
||||
pres := grypesarif.NewPresenter(models.PresenterConfig{Document: model, SBOM: scanResults.SBOM})
|
||||
if err := pres.Present(sp.writer); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,7 +167,7 @@ func (sp *SARIFPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.
|
||||
}
|
||||
|
||||
// image scan
|
||||
if err := sp.printImageScan(ctx, imageScanData[0].PresenterConfig); err != nil {
|
||||
if err := sp.printImageScan(ctx, imageScanData[0]); err != nil {
|
||||
logger.L().Ctx(ctx).Error("failed to write results in sarif format", helpers.Error(err))
|
||||
return
|
||||
}
|
||||
@@ -196,7 +199,7 @@ func (sp *SARIFPrinter) printConfigurationScan(ctx context.Context, opaSessionOb
|
||||
filepath := resourceSource.RelativePath
|
||||
|
||||
// Github Code Scanning considers results not associated to a file path meaningless and invalid when uploading
|
||||
if filepath == "" || basePath == "" {
|
||||
if filepath == "" && basePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package printer
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer"
|
||||
)
|
||||
@@ -18,9 +17,6 @@ func (silentPrinter *SilentPrinter) PrintNextSteps() {
|
||||
|
||||
}
|
||||
|
||||
func (silentPrinter *SilentPrinter) PrintImageScan(context.Context, *models.PresenterConfig) {
|
||||
}
|
||||
|
||||
func (silentPrinter *SilentPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData) {
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ package printer
|
||||
|
||||
import (
|
||||
v5 "github.com/anchore/grype/grype/db/v5"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/kubescape/v3/core/cautils"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils"
|
||||
"github.com/kubescape/opa-utils/reporthandling"
|
||||
"github.com/kubescape/opa-utils/reporthandling/apis"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/prioritization"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
||||
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
|
||||
@@ -16,6 +17,168 @@ import (
|
||||
|
||||
const indicator = "†"
|
||||
|
||||
// ControlSummaryWithSeverity wraps ControlSummary to add severity field for JSON output
|
||||
type ControlSummaryWithSeverity struct {
|
||||
reportsummary.ControlSummary
|
||||
Severity string `json:"severity"`
|
||||
}
|
||||
|
||||
// ResourceAssociatedControlWithSeverity wraps ResourceAssociatedControl to add severity field
|
||||
type ResourceAssociatedControlWithSeverity struct {
|
||||
resourcesresults.ResourceAssociatedControl
|
||||
Severity string `json:"severity"`
|
||||
}
|
||||
|
||||
// ResultWithSeverity wraps Result to include severity in associated controls
|
||||
type ResultWithSeverity struct {
|
||||
ResourceID string `json:"resourceID"`
|
||||
AssociatedControls []ResourceAssociatedControlWithSeverity `json:"controls,omitempty"`
|
||||
PrioritizedResource *prioritization.PrioritizedResource `json:"prioritizedResource,omitempty"`
|
||||
}
|
||||
|
||||
// SummaryDetailsWithSeverity wraps SummaryDetails to include enriched controls
|
||||
type SummaryDetailsWithSeverity struct {
|
||||
Controls map[string]ControlSummaryWithSeverity `json:"controls,omitempty"`
|
||||
Status apis.ScanningStatus `json:"status"`
|
||||
Frameworks []reportsummary.FrameworkSummary `json:"frameworks"`
|
||||
ResourcesSeverityCounters reportsummary.SeverityCounters `json:"resourcesSeverityCounters,omitempty"`
|
||||
ControlsSeverityCounters reportsummary.SeverityCounters `json:"controlsSeverityCounters,omitempty"`
|
||||
StatusCounters reportsummary.StatusCounters `json:"ResourceCounters"`
|
||||
Vulnerabilities reportsummary.VulnerabilitySummary `json:"vulnerabilities,omitempty"`
|
||||
Score float32 `json:"score"`
|
||||
ComplianceScore float32 `json:"complianceScore"`
|
||||
}
|
||||
|
||||
// PostureReportWithSeverity wraps PostureReport to include severity in controls
|
||||
type PostureReportWithSeverity struct {
|
||||
ReportGenerationTime string `json:"generationTime"`
|
||||
ClusterAPIServerInfo interface{} `json:"clusterAPIServerInfo"`
|
||||
ClusterCloudProvider string `json:"clusterCloudProvider"`
|
||||
CustomerGUID string `json:"customerGUID"`
|
||||
ClusterName string `json:"clusterName"`
|
||||
SummaryDetails SummaryDetailsWithSeverity `json:"summaryDetails,omitempty"`
|
||||
Resources []reporthandling.Resource `json:"resources,omitempty"`
|
||||
Attributes []reportsummary.PostureAttributes `json:"attributes"`
|
||||
Results []ResultWithSeverity `json:"results,omitempty"`
|
||||
Metadata reporthandlingv2.Metadata `json:"metadata,omitempty"`
|
||||
ResourceLabels map[string]map[string]string `json:"resourceLabels,omitempty"` // map[resourceID]map[labelKey]labelValue - extracted labels from workloads
|
||||
}
|
||||
|
||||
// enrichControlsWithSeverity adds severity field to controls based on scoreFactor
|
||||
func enrichControlsWithSeverity(controls reportsummary.ControlSummaries) map[string]ControlSummaryWithSeverity {
|
||||
enrichedControls := make(map[string]ControlSummaryWithSeverity)
|
||||
for controlID, control := range controls {
|
||||
enrichedControl := ControlSummaryWithSeverity{
|
||||
ControlSummary: control,
|
||||
Severity: apis.ControlSeverityToString(control.GetScoreFactor()),
|
||||
}
|
||||
enrichedControls[controlID] = enrichedControl
|
||||
}
|
||||
return enrichedControls
|
||||
}
|
||||
|
||||
// enrichResultsWithSeverity adds severity field to controls in results
|
||||
func enrichResultsWithSeverity(results []resourcesresults.Result, controlSummaries reportsummary.ControlSummaries) []ResultWithSeverity {
|
||||
enrichedResults := make([]ResultWithSeverity, len(results))
|
||||
for i, result := range results {
|
||||
enrichedControls := make([]ResourceAssociatedControlWithSeverity, len(result.AssociatedControls))
|
||||
for j, control := range result.AssociatedControls {
|
||||
// Get the severity from the control summary
|
||||
severity := "Unknown"
|
||||
if controlSummary, exists := controlSummaries[control.GetID()]; exists {
|
||||
severity = apis.ControlSeverityToString(controlSummary.GetScoreFactor())
|
||||
}
|
||||
enrichedControls[j] = ResourceAssociatedControlWithSeverity{
|
||||
ResourceAssociatedControl: control,
|
||||
Severity: severity,
|
||||
}
|
||||
}
|
||||
enrichedResults[i] = ResultWithSeverity{
|
||||
ResourceID: result.ResourceID,
|
||||
AssociatedControls: enrichedControls,
|
||||
PrioritizedResource: result.PrioritizedResource,
|
||||
}
|
||||
}
|
||||
return enrichedResults
|
||||
}
|
||||
|
||||
// ConvertToPostureReportWithSeverity converts PostureReport to PostureReportWithSeverity
|
||||
func ConvertToPostureReportWithSeverity(report *reporthandlingv2.PostureReport) *PostureReportWithSeverity {
|
||||
return ConvertToPostureReportWithSeverityAndLabels(report, nil, nil)
|
||||
}
|
||||
|
||||
// ConvertToPostureReportWithSeverityAndLabels converts PostureReport to PostureReportWithSeverity
|
||||
// and extracts specified labels from workloads
|
||||
func ConvertToPostureReportWithSeverityAndLabels(report *reporthandlingv2.PostureReport, labelsToCopy []string, allResources map[string]workloadinterface.IMetadata) *PostureReportWithSeverity {
|
||||
if report == nil {
|
||||
return nil
|
||||
}
|
||||
enrichedControls := enrichControlsWithSeverity(report.SummaryDetails.Controls)
|
||||
enrichedResults := enrichResultsWithSeverity(report.Results, report.SummaryDetails.Controls)
|
||||
|
||||
// Extract labels from resources if labelsToCopy is specified
|
||||
var resourceLabels map[string]map[string]string
|
||||
if len(labelsToCopy) > 0 && allResources != nil {
|
||||
resourceLabels = extractResourceLabels(allResources, labelsToCopy)
|
||||
}
|
||||
|
||||
return &PostureReportWithSeverity{
|
||||
ReportGenerationTime: report.ReportGenerationTime.Format("2006-01-02T15:04:05Z07:00"),
|
||||
ClusterAPIServerInfo: report.ClusterAPIServerInfo,
|
||||
ClusterCloudProvider: report.ClusterCloudProvider,
|
||||
CustomerGUID: report.CustomerGUID,
|
||||
ClusterName: report.ClusterName,
|
||||
SummaryDetails: SummaryDetailsWithSeverity{
|
||||
Controls: enrichedControls,
|
||||
Status: report.SummaryDetails.Status,
|
||||
Frameworks: report.SummaryDetails.Frameworks,
|
||||
ResourcesSeverityCounters: report.SummaryDetails.ResourcesSeverityCounters,
|
||||
ControlsSeverityCounters: report.SummaryDetails.ControlsSeverityCounters,
|
||||
StatusCounters: report.SummaryDetails.StatusCounters,
|
||||
Vulnerabilities: report.SummaryDetails.Vulnerabilities,
|
||||
Score: report.SummaryDetails.Score,
|
||||
ComplianceScore: report.SummaryDetails.ComplianceScore,
|
||||
},
|
||||
Resources: report.Resources,
|
||||
Attributes: report.Attributes,
|
||||
Results: enrichedResults,
|
||||
Metadata: report.Metadata,
|
||||
ResourceLabels: resourceLabels,
|
||||
}
|
||||
}
|
||||
|
||||
// extractResourceLabels extracts specified labels from all resources
|
||||
func extractResourceLabels(allResources map[string]workloadinterface.IMetadata, labelsToCopy []string) map[string]map[string]string {
|
||||
resourceLabels := make(map[string]map[string]string)
|
||||
|
||||
for resourceID, resource := range allResources {
|
||||
// IMetadata doesn't have GetLabels, need to cast to IBasicWorkload
|
||||
basicWorkload, ok := resource.(workloadinterface.IBasicWorkload)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := basicWorkload.GetLabels()
|
||||
if labels == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
extractedLabels := make(map[string]string)
|
||||
for _, labelKey := range labelsToCopy {
|
||||
if value, exists := labels[labelKey]; exists {
|
||||
extractedLabels[labelKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Only add to result if at least one label was found
|
||||
if len(extractedLabels) > 0 {
|
||||
resourceLabels[resourceID] = extractedLabels
|
||||
}
|
||||
}
|
||||
|
||||
return resourceLabels
|
||||
}
|
||||
|
||||
// FinalizeResults finalize the results objects by copying data from map to lists
|
||||
func FinalizeResults(data *cautils.OPASessionObj) *reporthandlingv2.PostureReport {
|
||||
report := reporthandlingv2.PostureReport{
|
||||
@@ -103,39 +266,39 @@ func setSeverityToSummaryMap(cves []imageprinter.CVE, mapSeverityToSummary map[s
|
||||
}
|
||||
}
|
||||
|
||||
func setPkgNameToScoreMap(matches []models.Match, pkgScores map[string]*imageprinter.PackageScore) {
|
||||
for i := range matches {
|
||||
func setPkgNameToScoreMap(matches match.Matches, pkgScores map[string]*imageprinter.PackageScore) {
|
||||
for _, m := range matches.Sorted() {
|
||||
// key is pkg name + version to avoid version conflicts
|
||||
key := matches[i].Artifact.Name + matches[i].Artifact.Version
|
||||
key := m.Package.Name + m.Package.Version
|
||||
|
||||
if _, ok := pkgScores[key]; !ok {
|
||||
pkgScores[key] = &imageprinter.PackageScore{
|
||||
Version: matches[i].Artifact.Version,
|
||||
Name: matches[i].Artifact.Name,
|
||||
Version: m.Package.Version,
|
||||
Name: m.Package.Name,
|
||||
MapSeverityToCVEsNumber: make(map[string]int, 0),
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := pkgScores[key].MapSeverityToCVEsNumber[matches[i].Vulnerability.Severity]; !ok {
|
||||
pkgScores[key].MapSeverityToCVEsNumber[matches[i].Vulnerability.Severity] = 1
|
||||
if _, ok := pkgScores[key].MapSeverityToCVEsNumber[m.Vulnerability.Metadata.Severity]; !ok {
|
||||
pkgScores[key].MapSeverityToCVEsNumber[m.Vulnerability.Metadata.Severity] = 1
|
||||
} else {
|
||||
pkgScores[key].MapSeverityToCVEsNumber[matches[i].Vulnerability.Severity] += 1
|
||||
pkgScores[key].MapSeverityToCVEsNumber[m.Vulnerability.Metadata.Severity] += 1
|
||||
}
|
||||
|
||||
pkgScores[key].Score += utils.ImageSeverityToInt(matches[i].Vulnerability.Severity)
|
||||
pkgScores[key].Score += utils.ImageSeverityToInt(m.Vulnerability.Metadata.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func extractCVEs(matches []models.Match) []imageprinter.CVE {
|
||||
CVEs := []imageprinter.CVE{}
|
||||
for i := range matches {
|
||||
func extractCVEs(matches match.Matches) []imageprinter.CVE {
|
||||
var CVEs []imageprinter.CVE
|
||||
for _, m := range matches.Sorted() {
|
||||
cve := imageprinter.CVE{
|
||||
ID: matches[i].Vulnerability.ID,
|
||||
Severity: matches[i].Vulnerability.Severity,
|
||||
Package: matches[i].Artifact.Name,
|
||||
Version: matches[i].Artifact.Version,
|
||||
FixVersions: matches[i].Vulnerability.Fix.Versions,
|
||||
FixedState: matches[i].Vulnerability.Fix.State,
|
||||
ID: m.Vulnerability.Metadata.ID,
|
||||
Severity: m.Vulnerability.Metadata.Severity,
|
||||
Package: m.Package.Name,
|
||||
Version: m.Package.Version,
|
||||
FixVersions: m.Vulnerability.Fix.Versions,
|
||||
FixedState: m.Vulnerability.Fix.State.String(),
|
||||
}
|
||||
CVEs = append(CVEs, cve)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
v5 "github.com/anchore/grype/grype/db/v5"
|
||||
"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/kubescape/k8s-interface/workloadinterface"
|
||||
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -12,29 +16,30 @@ import (
|
||||
func TestExtractCVEs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
matches []models.Match
|
||||
matches match.Matches
|
||||
want []imageprinter.CVE
|
||||
}{
|
||||
{
|
||||
name: "single vuln",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "foo",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
ID: "CVE-2020-1234",
|
||||
Severity: "High",
|
||||
},
|
||||
Fix: models.Fix{
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{"1.2.3"},
|
||||
State: "Fixed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
want: []imageprinter.CVE{
|
||||
{
|
||||
ID: "CVE-2020-1234",
|
||||
@@ -48,56 +53,59 @@ func TestExtractCVEs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple vulns",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "foo",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
ID: "CVE-2020-1234",
|
||||
Severity: "High",
|
||||
},
|
||||
Fix: models.Fix{
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{"1.2.3"},
|
||||
State: "Fixed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "2",
|
||||
Name: "test",
|
||||
Version: "1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
ID: "CVE-2020-1235",
|
||||
Severity: "Critical",
|
||||
},
|
||||
Fix: models.Fix{
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{"1"},
|
||||
State: "Fixed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "3",
|
||||
Name: "test2",
|
||||
Version: "3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
ID: "CVE-2020-1236",
|
||||
Severity: "Low",
|
||||
},
|
||||
Fix: models.Fix{
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{"2", "3", "4"},
|
||||
State: "Not fixed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
want: []imageprinter.CVE{
|
||||
{
|
||||
ID: "CVE-2020-1234",
|
||||
@@ -127,7 +135,7 @@ func TestExtractCVEs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "empty vulns",
|
||||
matches: []models.Match{},
|
||||
matches: match.NewMatches([]match.Match{}...),
|
||||
want: []imageprinter.CVE{},
|
||||
},
|
||||
}
|
||||
@@ -171,25 +179,26 @@ func TestExtractCVEs(t *testing.T) {
|
||||
func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
matches []models.Match
|
||||
matches match.Matches
|
||||
originalMap map[string]*imageprinter.PackageScore
|
||||
want map[string]*imageprinter.PackageScore
|
||||
}{
|
||||
{
|
||||
name: "single package",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "foo",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
want: map[string]*imageprinter.PackageScore{
|
||||
"foo1.2.3": {
|
||||
Name: "foo",
|
||||
@@ -203,41 +212,44 @@ func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple packages - different versions",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "2",
|
||||
Name: "pkg2",
|
||||
Version: "1.2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Low",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "3",
|
||||
Name: "pkg3",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
want: map[string]*imageprinter.PackageScore{
|
||||
"pkg1version1": {
|
||||
Name: "pkg1",
|
||||
@@ -267,74 +279,80 @@ func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple packages - mixed versions",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "2",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "3",
|
||||
Name: "pkg1",
|
||||
Version: "version2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "4",
|
||||
Name: "pkg3",
|
||||
Version: "1.2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Medium",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "5",
|
||||
Name: "pkg3",
|
||||
Version: "1.2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Low",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "6",
|
||||
Name: "pkg4",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
want: map[string]*imageprinter.PackageScore{
|
||||
"pkg1version1": {
|
||||
Name: "pkg1",
|
||||
@@ -373,46 +391,49 @@ func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "empty packages",
|
||||
matches: []models.Match{},
|
||||
matches: match.NewMatches(),
|
||||
want: map[string]*imageprinter.PackageScore{},
|
||||
},
|
||||
{
|
||||
name: "original map not empty",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "pkg1",
|
||||
Version: "version2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "2",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "3",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
originalMap: map[string]*imageprinter.PackageScore{
|
||||
"pkg41.2.3": {
|
||||
Name: "pkg4",
|
||||
@@ -452,41 +473,44 @@ func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "original map with same package",
|
||||
matches: []models.Match{
|
||||
matches: match.NewMatches([]match.Match{
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "1",
|
||||
Name: "pkg1",
|
||||
Version: "version2",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "Critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "2",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Artifact: models.Package{
|
||||
Package: pkg.Package{
|
||||
ID: "3",
|
||||
Name: "pkg1",
|
||||
Version: "version1",
|
||||
},
|
||||
Vulnerability: models.Vulnerability{
|
||||
VulnerabilityMetadata: models.VulnerabilityMetadata{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
Metadata: &vulnerability.Metadata{
|
||||
Severity: "High",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
originalMap: map[string]*imageprinter.PackageScore{
|
||||
"pkg1version1": {
|
||||
Name: "pkg1",
|
||||
@@ -518,37 +542,37 @@ func TestSetPkgNameToScoreMap(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
t.Run(tests[i].name, func(t *testing.T) {
|
||||
if tests[i].originalMap == nil {
|
||||
tests[i].originalMap = make(map[string]*imageprinter.PackageScore)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.originalMap == nil {
|
||||
tt.originalMap = make(map[string]*imageprinter.PackageScore)
|
||||
}
|
||||
|
||||
setPkgNameToScoreMap(tests[i].matches, tests[i].originalMap)
|
||||
if len(tests[i].originalMap) == 0 {
|
||||
assert.Equal(t, tests[i].want, tests[i].originalMap)
|
||||
setPkgNameToScoreMap(tt.matches, tt.originalMap)
|
||||
if len(tt.originalMap) == 0 {
|
||||
assert.Equal(t, tt.want, tt.originalMap)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tests[i].originalMap) != len(tests[i].want) {
|
||||
t.Errorf("%s failed for length, got = %v, want %v", tests[i].name, len(tests[i].originalMap), len(tests[i].want))
|
||||
if len(tt.originalMap) != len(tt.want) {
|
||||
t.Errorf("%s failed for length, got = %v, want %v", tt.name, len(tt.originalMap), len(tt.want))
|
||||
}
|
||||
|
||||
for k := range tests[i].originalMap {
|
||||
if tests[i].originalMap[k].Score != tests[i].want[k].Score {
|
||||
t.Errorf("%s failed for score, got = %v, want %v", tests[i].name, tests[i].want[k].Score, tests[i].originalMap[k].Score)
|
||||
for k := range tt.originalMap {
|
||||
if tt.originalMap[k].Score != tt.want[k].Score {
|
||||
t.Errorf("%s failed for score, got = %v, want %v", tt.name, tt.want[k].Score, tt.originalMap[k].Score)
|
||||
}
|
||||
if tests[i].originalMap[k].Version != tests[i].want[k].Version {
|
||||
t.Errorf("%s failed for version, got = %v, want %v", tests[i].name, tests[i].want[k].Version, tests[i].originalMap[k].Version)
|
||||
if tt.originalMap[k].Version != tt.want[k].Version {
|
||||
t.Errorf("%s failed for version, got = %v, want %v", tt.name, tt.want[k].Version, tt.originalMap[k].Version)
|
||||
|
||||
}
|
||||
if tests[i].originalMap[k].Name != tests[i].want[k].Name {
|
||||
t.Errorf("%s failed for name, got = %v, want %v", tests[i].name, tests[i].want[k].Name, tests[i].originalMap[k].Name)
|
||||
if tt.originalMap[k].Name != tt.want[k].Name {
|
||||
t.Errorf("%s failed for name, got = %v, want %v", tt.name, tt.want[k].Name, tt.originalMap[k].Name)
|
||||
}
|
||||
|
||||
for s := range tests[i].originalMap[k].MapSeverityToCVEsNumber {
|
||||
if tests[i].originalMap[k].MapSeverityToCVEsNumber[s] != tests[i].want[k].MapSeverityToCVEsNumber[s] {
|
||||
t.Errorf("%s failed for severity %s, got = %v, want %v", tests[i].name, s, tests[i].want[k].MapSeverityToCVEsNumber[s], tests[i].originalMap[k].MapSeverityToCVEsNumber[s])
|
||||
for s := range tt.originalMap[k].MapSeverityToCVEsNumber {
|
||||
if tt.originalMap[k].MapSeverityToCVEsNumber[s] != tt.want[k].MapSeverityToCVEsNumber[s] {
|
||||
t.Errorf("%s failed for severity %s, got = %v, want %v", tt.name, s, tt.want[k].MapSeverityToCVEsNumber[s], tt.originalMap[k].MapSeverityToCVEsNumber[s])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,3 +744,131 @@ func TestSetSeverityToSummaryMap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createWorkloadWithLabels(name, namespace string, labels map[string]string) workloadinterface.IMetadata {
|
||||
// Convert labels to map[string]interface{} for JSON marshaling
|
||||
labelsInterface := make(map[string]interface{})
|
||||
for k, v := range labels {
|
||||
labelsInterface[k] = v
|
||||
}
|
||||
|
||||
obj := map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
"labels": labelsInterface,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"template": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
objBytes, _ := json.Marshal(obj)
|
||||
workload, _ := workloadinterface.NewWorkload(objBytes)
|
||||
return workload
|
||||
}
|
||||
|
||||
func TestExtractResourceLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allResources map[string]workloadinterface.IMetadata
|
||||
labelsToCopy []string
|
||||
want map[string]map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty resources",
|
||||
allResources: map[string]workloadinterface.IMetadata{},
|
||||
labelsToCopy: []string{"app", "team"},
|
||||
want: map[string]map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "empty labels to copy",
|
||||
allResources: map[string]workloadinterface.IMetadata{},
|
||||
labelsToCopy: []string{},
|
||||
want: map[string]map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "single resource with matching labels",
|
||||
allResources: map[string]workloadinterface.IMetadata{
|
||||
"resource-1": createWorkloadWithLabels("test-deploy", "default", map[string]string{
|
||||
"app": "myapp",
|
||||
"team": "platform",
|
||||
"version": "v1",
|
||||
}),
|
||||
},
|
||||
labelsToCopy: []string{"app", "team"},
|
||||
want: map[string]map[string]string{
|
||||
"resource-1": {
|
||||
"app": "myapp",
|
||||
"team": "platform",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single resource with partial matching labels",
|
||||
allResources: map[string]workloadinterface.IMetadata{
|
||||
"resource-1": createWorkloadWithLabels("test-deploy", "default", map[string]string{
|
||||
"app": "myapp",
|
||||
}),
|
||||
},
|
||||
labelsToCopy: []string{"app", "team"},
|
||||
want: map[string]map[string]string{
|
||||
"resource-1": {
|
||||
"app": "myapp",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single resource with no matching labels",
|
||||
allResources: map[string]workloadinterface.IMetadata{
|
||||
"resource-1": createWorkloadWithLabels("test-deploy", "default", map[string]string{
|
||||
"version": "v1",
|
||||
}),
|
||||
},
|
||||
labelsToCopy: []string{"app", "team"},
|
||||
want: map[string]map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "multiple resources with various labels",
|
||||
allResources: map[string]workloadinterface.IMetadata{
|
||||
"resource-1": createWorkloadWithLabels("deploy-1", "default", map[string]string{
|
||||
"app": "app1",
|
||||
"team": "team1",
|
||||
}),
|
||||
"resource-2": createWorkloadWithLabels("deploy-2", "default", map[string]string{
|
||||
"app": "app2",
|
||||
}),
|
||||
"resource-3": createWorkloadWithLabels("deploy-3", "default", map[string]string{
|
||||
"version": "v1",
|
||||
}),
|
||||
},
|
||||
labelsToCopy: []string{"app", "team"},
|
||||
want: map[string]map[string]string{
|
||||
"resource-1": {
|
||||
"app": "app1",
|
||||
"team": "team1",
|
||||
},
|
||||
"resource-2": {
|
||||
"app": "app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractResourceLabels(tt.allResources, tt.labelsToCopy)
|
||||
assert.Equal(t, len(tt.want), len(got), "number of resources with extracted labels should match")
|
||||
for resourceID, wantLabels := range tt.want {
|
||||
gotLabels, ok := got[resourceID]
|
||||
assert.True(t, ok, "resource %s should be present in result", resourceID)
|
||||
assert.Equal(t, wantLabels, gotLabels, "labels for resource %s should match", resourceID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user