Compare commits

..

1 Commits

Author SHA1 Message Date
Amir Malka
2d70fc111b Update MAINTAINERS.md
Signed-off-by: Amir Malka <amirm@armosec.io>
2024-11-26 10:46:45 +02:00
253 changed files with 26876 additions and 54277 deletions

View File

@@ -1,243 +1,71 @@
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/*"
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths-ignore:
- '**.yaml'
- '**.yml'
- '**.md'
- '**.sh'
- 'website/*'
- 'examples/*'
- 'docs/*'
- 'build/*'
- '.github/*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-scanner:
permissions:
actions: read
artifact-metadata: read
attestations: read
checks: read
contents: write
deployments: read
discussions: read
id-token: write
issues: read
models: read
packages: read
pages: read
pull-requests: write
repository-projects: read
security-events: read
statuses: read
uses: ./.github/workflows/a-pr-scanner.yaml
with:
RELEASE: ""
CLIENT: test
CGO_ENABLED: 0
GO111MODULE: ""
secrets: inherit
pr-scanner:
permissions:
actions: read
checks: read
deployments: read
id-token: write
issues: read
discussions: 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: ""
CLIENT: test
CGO_ENABLED: 0
GO111MODULE: ""
secrets: inherit
wf-preparation:
name: secret-validator
runs-on: ubuntu-latest
outputs:
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"
run-system-tests:
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
contents: read
pull-requests: write
steps:
- name: Set dispatch info
id: dispatch-info
run: |
# Correlation ID WITHOUT attempt - so re-runs can find the original run
CORRELATION_ID="${GITHUB_REPOSITORY##*/}-${{ github.run_id }}"
echo "correlation_id=${CORRELATION_ID}" >> "$GITHUB_OUTPUT"
echo "Correlation ID: ${CORRELATION_ID}, Attempt: ${{ github.run_attempt }}"
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.E2E_DISPATCH_APP_ID }}
private-key: ${{ secrets.E2E_DISPATCH_APP_PRIVATE_KEY }}
owner: armosec
repositories: shared-workflows
- name: Dispatch system tests to private repo
if: ${{ github.run_attempt == 1 }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CORRELATION_ID: ${{ steps.dispatch-info.outputs.correlation_id }}
KS_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
echo "Dispatching E2E tests with correlation_id: ${CORRELATION_ID}"
echo "Using test group: KUBESCAPE_CLI_E2E"
gh api "repos/armosec/shared-workflows/dispatches" \
-f event_type="e2e-test-trigger" \
-f "client_payload[correlation_id]=${CORRELATION_ID}" \
-f "client_payload[github_repository]=${GITHUB_REPOSITORY}" \
-f "client_payload[environment]=production" \
-f "client_payload[tests_groups]=KUBESCAPE_CLI_E2E" \
-f "client_payload[systests_branch]=master" \
-f "client_payload[ks_branch]=${KS_BRANCH}"
echo "Dispatch completed"
- name: Find E2E workflow run
id: find-run
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CORRELATION_ID: ${{ steps.dispatch-info.outputs.correlation_id }}
run: |
for i in {1..15}; do
run_id=$(gh api "repos/armosec/shared-workflows/actions/runs?event=repository_dispatch&per_page=30" \
--jq '.workflow_runs | map(select(.name | contains("'"$CORRELATION_ID"'"))) | first | .id // empty')
if [ -n "$run_id" ]; then
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
gh api "repos/armosec/shared-workflows/actions/runs/${run_id}" --jq '"url=" + .html_url' >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Attempt $i: waiting for run..."
sleep $((i < 5 ? 10 : 30))
done
echo "::error::Could not find workflow run"
exit 1
- name: Re-run failed jobs in private repo
id: rerun
if: ${{ github.run_attempt > 1 }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RUN_ID: ${{ steps.find-run.outputs.run_id }}
run: |
conclusion=$(gh api "repos/armosec/shared-workflows/actions/runs/${RUN_ID}" --jq '.conclusion')
echo "Previous conclusion: $conclusion"
if [ "$conclusion" = "success" ]; then
echo "Previous run passed. Nothing to re-run."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Full rerun if cancelled, partial if failed
if [ "$conclusion" = "cancelled" ]; then
echo "Run was cancelled - triggering full re-run"
gh api --method POST "repos/armosec/shared-workflows/actions/runs/${RUN_ID}/rerun"
else
echo "Re-running failed jobs only"
gh api --method POST "repos/armosec/shared-workflows/actions/runs/${RUN_ID}/rerun-failed-jobs"
fi
# Wait for status to flip from 'completed'
for i in {1..30}; do
[ "$(gh api "repos/armosec/shared-workflows/actions/runs/${RUN_ID}" --jq '.status')" != "completed" ] && break
sleep 2
done
- name: Wait for E2E tests to complete
if: ${{ steps.rerun.outputs.skip != 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RUN_ID: ${{ steps.find-run.outputs.run_id }}
URL: ${{ steps.find-run.outputs.url }}
run: |
echo "Monitoring: ${URL}"
for i in {1..60}; do # 60 iterations × 60s = 1 hour max
read status conclusion < <(gh api "repos/armosec/shared-workflows/actions/runs/${RUN_ID}" \
--jq '[.status, .conclusion // "null"] | @tsv')
echo "Status: ${status} | Conclusion: ${conclusion}"
if [ "$status" = "completed" ]; then
if [ "$conclusion" = "success" ]; then
echo "E2E tests passed!"
exit 0
fi
echo "::error::E2E tests failed: ${conclusion}"
echo ""
# Get failed job IDs to a file first
gh api "repos/armosec/shared-workflows/actions/runs/${RUN_ID}/jobs" \
--jq '.jobs[] | select(.conclusion == "failure") | [.id, .name, (.steps[] | select(.conclusion == "failure") | .name)] | @tsv' > /tmp/failed_jobs.txt
# Process each failed job
while IFS=$'\t' read -r job_id job_name step_name; do
# Extract test name: "run-helm-e2e / ST (relevancy_python)" → "relevancy_python"
test_name=$(echo "$job_name" | sed 's/.*(\(.*\))/\1/')
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "${job_name}"
echo " Step: ${step_name}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Fetch logs to temp file
gh api "repos/armosec/shared-workflows/actions/jobs/${job_id}/logs" 2>/dev/null > /tmp/job_logs.txt
# Show summary in console
grep -E "(ERROR|FAILURE)" /tmp/job_logs.txt | tail -10
echo ""
# Save to separate file per test
log_file="failed_${test_name}.txt"
echo "════════════════════════════════════════" > "$log_file"
echo "${job_name}" >> "$log_file"
echo " Step: ${step_name}" >> "$log_file"
echo "════════════════════════════════════════" >> "$log_file"
last_endgroup=$(grep -n "##\\[endgroup\\]" /tmp/job_logs.txt | tail -1 | cut -d: -f1)
if [ -n "$last_endgroup" ]; then
tail -n +$((last_endgroup + 1)) /tmp/job_logs.txt >> "$log_file"
else
tail -500 /tmp/job_logs.txt >> "$log_file"
fi
done < /tmp/failed_jobs.txt
echo "View full logs: ${URL}"
exit 1
fi
sleep 60
done
echo "::error::Timeout waiting for tests"
exit 1
- name: Upload failed step logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: failed-e2e-logs-attempt-${{ github.run_attempt }}
path: failed_*.txt
retention-days: 7
binary-build:
if: ${{ github.actor == 'kubescape' }}
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.21"
RELEASE: "latest"
CLIENT: test
secrets: inherit

View File

@@ -3,16 +3,45 @@ permissions: read-all
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
inputs:
skip_publish:
description: "Skip publishing artifacts"
required: false
default: true
type: boolean
- 'v*.*.*-rc.*'
jobs:
release:
retag:
outputs:
NEW_TAG: ${{ steps.tag-calculator.outputs.NEW_TAG }}
runs-on: ubuntu22-core4-mem16-ssd150
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.21"
RELEASE: ${{ needs.retag.outputs.NEW_TAG }}
CLIENT: release
secrets: inherit
create-release:
permissions:
actions: read
checks: read
@@ -21,105 +50,63 @@ jobs:
discussions: read
id-token: write
issues: read
models: read
packages: write
packages: read
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
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.9"
- name: Install system dependencies for system-tests
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libpq5 \
libpq-dev \
gcc \
python3-dev
sudo rm -rf /var/lib/apt/lists/*
- name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0
- name: Create Cosign Key
run: echo "${{ secrets.COSIGN_PRIVATE_KEY_V1 }}" > cosign.key
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Quay.io
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
password: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
- uses: anchore/sbom-action/download-syft@v0
name: Setup Syft
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.10.0
with:
cluster_name: kubescape-e2e
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean ${{ inputs.skip_publish == true && '--skip=publish' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
COSIGN_PWD: ${{ secrets.COSIGN_PRIVATE_KEY_V1_PASSWORD }}
RELEASE: ${{ github.ref_name }}
CLIENT: release
RUN_E2E: "true"
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
CUSTOMER: ${{ secrets.CUSTOMER }}
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.PASSWORD }}
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
- name: Update new version in krew-index
if: github.event_name != 'workflow_dispatch' || inputs.skip_publish != true
uses: rajatjindal/krew-release-bot@v0.0.47
with:
krew_template_file: .krew.yaml
- 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
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

View File

@@ -27,7 +27,7 @@ jobs:
name: Create cross-platform build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-large
runs-on: ubuntu22-core4-mem16-ssd150
steps:
- uses: actions/checkout@v4
@@ -39,6 +39,7 @@ jobs:
name: Installing go
with:
go-version: ${{ inputs.GO_VERSION }}
cache: true
- name: Test core pkg
run: ${{ env.DOCKER_CMD }} go test -v ./...
@@ -48,15 +49,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
- uses: anchore/sbom-action/download-syft@v0.15.2
name: Setup Syft
- uses: goreleaser/goreleaser-action@v6
- uses: goreleaser/goreleaser-action@v5
name: Build
with:
distribution: goreleaser
version: latest
args: build --clean --snapshot --single-target
args: release --clean --snapshot
env:
RELEASE: ${{ inputs.RELEASE }}
CLIENT: ${{ inputs.CLIENT }}
@@ -66,11 +67,87 @@ jobs:
env:
RELEASE: ${{ inputs.RELEASE }}
KUBESCAPE_SKIP_UPDATE_CHECK: "true"
run: ${{ env.DOCKER_CMD }} python3 smoke_testing/init.py ${PWD}/dist/cli_linux_amd64_v1/kubescape
run: ${{ env.DOCKER_CMD }} python3 smoke_testing/init.py ${PWD}/dist/kubescape-ubuntu-latest
- name: golangci-lint
continue-on-error: false
uses: golangci/golangci-lint-action@v9
uses: golangci/golangci-lint-action@v3
with:
version: latest
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.21'
cache: true
- name: Scanning - Forbidden Licenses (go-licenses)
id: licenses-scan
continue-on-error: true
run: |
echo "## Installing go-licenses tool"
go install github.com/google/go-licenses@latest
echo "## Scanning for forbiden licenses ##"
go-licenses check .
- name: Scanning - Credentials (GitGuardian)
if: ${{ env.GITGUARDIAN_API_KEY }}
continue-on-error: true
id: credentials-scan
uses: GitGuardian/ggshield-action@4ab2994172fadab959240525e6b833d9ae3aca61 # ratchet:GitGuardian/ggshield-action@master
with:
args: -v --all-policies
env:
GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
- name: Scanning - Vulnerabilities (Snyk)
if: ${{ env.SNYK_TOKEN }}
id: vulnerabilities-scan
continue-on-error: true
uses: snyk/actions/golang@806182742461562b67788a64410098c9d9b96adb # ratchet:snyk/actions/golang@master
with:
command: test --all-projects
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Test coverage
id: unit-test
run: go test -v ${{ inputs.UNIT_TESTS_PATH }} -covermode=count -coverprofile=coverage.out
- name: Convert coverage count to lcov format
uses: jandelgado/gcov2lcov-action@v1
- name: Submit coverage tests to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v1
with:
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
path-to-lcov: coverage.lcov
- name: Comment results to PR
continue-on-error: true # Warning: This might break opening PRs from forks
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # ratchet:peter-evans/create-or-update-comment@v2.1.0
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Scan results:
- License scan: ${{ steps.licenses-scan.outcome }}
- Credentials scan: ${{ steps.credentials-scan.outcome }}
- Vulnerabilities scan: ${{ steps.vulnerabilities-scan.outcome }}
reactions: 'eyes'

View File

@@ -0,0 +1,360 @@
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 }}
cache: true
- 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.15.2
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@83fd05a356d7e2593de66fc9913b3002723633cb # ratchet:actions/upload-artifact@v3.1.1
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@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
id: download-artifact
with:
name: kubescape
path: "~"
- run: ls -laR
- name: chmod +x
run: chmod +x -R ${{steps.download-artifact.outputs.download-path}}/kubescape-ubuntu-latest
- name: Checkout systests repo
uses: actions/checkout@v4
with:
repository: armosec/system-tests
path: .
- uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # ratchet:actions/setup-python@v4
with:
python-version: '3.8.13'
cache: 'pip'
- name: create env
run: ./create_env.sh
- name: Generate uuid
id: uuid
run: |
echo "RANDOM_UUID=$(uuidgen)" >> $GITHUB_OUTPUT
- name: Create k8s Kind Cluster
id: kind-cluster-install
uses: helm/kind-action@d08cf6ff1575077dee99962540d77ce91c62387d # ratchet:helm/kind-action@v1.3.0
with:
cluster_name: ${{ steps.uuid.outputs.RANDOM_UUID }}
- name: run-tests-on-local-built-kubescape
env:
CUSTOMER: ${{ secrets.CUSTOMER }}
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.PASSWORD }}
CLIENT_ID: ${{ secrets.CLIENT_ID_PROD }}
SECRET_KEY: ${{ secrets.SECRET_KEY_PROD }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
echo "Test history:"
echo " ${{ matrix.TEST }} " >/tmp/testhistory
cat /tmp/testhistory
source systests_python_env/bin/activate
python3 systest-cli.py \
-t ${{ matrix.TEST }} \
-b production \
-c CyberArmorTests \
--duration 3 \
--logger DEBUG \
--kwargs kubescape=${{steps.download-artifact.outputs.download-path}}/kubescape-ubuntu-latest
deactivate
- name: Test Report
uses: mikepenz/action-junit-report@6e9933f4a97f4d2b99acef4d7b97924466037882 # ratchet:mikepenz/action-junit-report@v3.6.1
if: always() # always run even if the previous step fails
with:
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 Normal file
View File

@@ -0,0 +1,41 @@
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.21"
REQUIRED_TESTS: '[]'
COSIGN: ${{ inputs.CO_SIGN }}
HELM_E2E_TEST: false
FORCE: true
secrets: inherit

91
.github/workflows/c-create-release.yaml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: c-create_release
permissions: read-all
on:
workflow_call:
inputs:
RELEASE_NAME:
description: 'Release name'
required: true
type: string
TAG:
description: 'Tag name'
required: true
type: string
DRAFT:
description: 'Create draft release'
required: false
type: boolean
default: false
jobs:
create-release:
name: create-release
runs-on: ubuntu-latest
env:
MAC_OS: macos-latest
UBUNTU_OS: ubuntu-latest
WINDOWS_OS: windows-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
id: download-artifact
with:
path: .
# TODO: kubescape-windows-latest is deprecated and should be removed
- name: Get kubescape.exe from kubescape-windows-latest.exe
run: cp ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-${{ env.WINDOWS_OS }}.exe ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape.exe
- name: Set release token
id: set-token
run: |
if [ "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" != "" ]; then
echo "token=${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" >> $GITHUB_OUTPUT;
else
echo "token=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_OUTPUT;
fi
- name: List artifacts
run: |
find . -type f -print
- name: Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
with:
token: ${{ steps.set-token.outputs.token }}
name: ${{ inputs.RELEASE_NAME }}
tag_name: ${{ inputs.TAG }}
body: ${{ github.event.pull_request.body }}
draft: ${{ inputs.DRAFT }}
prerelease: false
fail_on_unmatched_files: true
files: |
./kubescape/kubescape-${{ env.MAC_OS }}
./kubescape/kubescape-${{ env.MAC_OS }}.sbom
./kubescape/kubescape-${{ env.MAC_OS }}.sha256
./kubescape/kubescape-${{ env.MAC_OS }}.tar.gz
./kubescape/kubescape-${{ env.UBUNTU_OS }}
./kubescape/kubescape-${{ env.UBUNTU_OS }}.sbom
./kubescape/kubescape-${{ env.UBUNTU_OS }}.sha256
./kubescape/kubescape-${{ env.UBUNTU_OS }}.tar.gz
./kubescape/kubescape-${{ env.WINDOWS_OS }}.exe
./kubescape/kubescape-${{ env.WINDOWS_OS }}.exe.sbom
./kubescape/kubescape-${{ env.WINDOWS_OS }}.exe.sha256
./kubescape/kubescape-${{ env.WINDOWS_OS }}.tar.gz
./kubescape/kubescape-arm64-${{ env.MAC_OS }}
./kubescape/kubescape-arm64-${{ env.MAC_OS }}.sbom
./kubescape/kubescape-arm64-${{ env.MAC_OS }}.sha256
./kubescape/kubescape-arm64-${{ env.MAC_OS }}.tar.gz
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}.sbom
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}.sha256
./kubescape/kubescape-arm64-${{ env.UBUNTU_OS }}.tar.gz
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.exe
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.exe.sbom
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.exe.sha256
./kubescape/kubescape-arm64-${{ env.WINDOWS_OS }}.tar.gz
./kubescape/kubescape-riscv64-${{ env.UBUNTU_OS }}
./kubescape/kubescape-riscv64-${{ env.UBUNTU_OS }}.sbom
./kubescape/kubescape-riscv64-${{ env.UBUNTU_OS }}.sha256
./kubescape/kubescape-riscv64-${{ env.UBUNTU_OS }}.tar.gz
./kubescape/kubescape.exe

109
.github/workflows/d-publish-image.yaml vendored Normal file
View File

@@ -0,0 +1,109 @@
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@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # ratchet:docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f03ac48505955848960e80bbb68046aa35c7b9e7 # ratchet:docker/setup-buildx-action@v2
- name: Login to Quay.io
env:
QUAY_PASSWORD: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }}
QUAY_USERNAME: ${{ secrets.QUAYIO_REGISTRY_USERNAME }}
run: docker login -u="${QUAY_USERNAME}" -p="${QUAY_PASSWORD}" quay.io
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # ratchet:actions/download-artifact@v3.0.2
id: download-artifact
with:
path: .
- name: mv kubescape amd64 binary
run: mv ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-ubuntu-latest kubescape-amd64-ubuntu-latest
- name: mv kubescape arm64 binary
run: mv ${{steps.download-artifact.outputs.download-path}}/kubescape/kubescape-arm64-ubuntu-latest kubescape-arm64-ubuntu-latest
- name: chmod +x
run: chmod +x -v kubescape-a*
- name: Build and push images
run: docker buildx build . --file build/kubescape-cli.Dockerfile --tag ${{ inputs.image_name }}:${{ inputs.image_tag }} --tag ${{ inputs.image_name }}:latest --build-arg image_version=${{ inputs.image_tag }} --build-arg client=${{ inputs.client }} --push --platform linux/amd64,linux/arm64
- name: Install cosign
uses: sigstore/cosign-installer@main
with:
cosign-release: 'v2.2.2'
- name: sign kubescape container image
if: ${{ inputs.cosign }}
env:
COSIGN_EXPERIMENTAL: "true"
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY_V1 }}
COSIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.COSIGN_PRIVATE_KEY_V1_PASSWORD }}
COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY_V1 }}
run: |
# Sign the image with keyless mode
cosign sign -y ${{ inputs.image_name }}:${{ inputs.image_tag }}
# Sign the image with key for verifier clients without keyless support
# Put the key from environment variable to a file
echo "$COSIGN_PRIVATE_KEY" > cosign.key
printf "$COSIGN_PRIVATE_KEY_PASSWORD" | cosign sign -key cosign.key -y ${{ inputs.image_name }}:${{ inputs.image_tag }}
rm cosign.key
# Verify the image
echo "$COSIGN_PUBLIC_KEY" > cosign.pub
cosign verify -key cosign.pub ${{ inputs.image_name }}:${{ inputs.image_tag }}

46
.github/workflows/e-post-release.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
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 }}

View File

@@ -32,12 +32,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@v2.4.3
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
@@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
with:
name: SARIF file
path: results.sarif
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
with:
sarif_file: results.sarif

View File

@@ -0,0 +1,20 @@
permissions: read-all
on:
issues:
types: [opened, labeled]
jobs:
open_PR_message:
if: github.event.label.name == 'typo'
runs-on: ubuntu-latest
steps:
- uses: ben-z/actions-comment-on-issue@10be23f9c43ac792663043420fda29dde07e2f0f # ratchet:ben-z/actions-comment-on-issue@1.0.2
with:
message: "Hello! :wave:\n\nThis issue is being automatically closed, Please open a PR with a relevant fix."
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto_close_issues:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/close-matching-issues@e9e43aad2fa6f06a058cedfd8fb975fd93b56d8f # ratchet:lee-dohm/close-matching-issues@v2
with:
query: 'label:typo'
token: ${{ secrets.GITHUB_TOKEN }}

5
.gitignore vendored
View File

@@ -9,10 +9,5 @@
ca.srl
*.out
ks
cosign.key
dist/
# Test output files
customFilename.pdf
customFilename.xml

View File

@@ -1,57 +1,51 @@
version: "2"
linters-settings:
govet:
shadow: true
dupl:
threshold: 200
goconst:
min-len: 3
min-occurrences: 2
gocognit:
min-complexity: 65
linters:
enable:
- bodyclose
- gosec
- staticcheck
- nolintlint
- gofmt
- unused
- govet
- bodyclose
- typecheck
- goimports
- ineffassign
- gosimple
disable:
- dupl
# temporarily disabled
- errcheck
- gochecknoglobals
- gochecknoinits
- gocognit
- dupl
- gocritic
- lll
- gocognit
- nakedret
- revive
- stylecheck
- unconvert
- unparam
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$
#- 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"

View File

@@ -1,121 +1,46 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# The lines bellow are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
- go test -v ./...
- go -C httphandler test -v ./...
archives:
- id: cli
ids:
- cli
formats:
- binary
- tar.gz
builds:
- id: cli
binary: kubescape
env:
- CGO_ENABLED=0
goos:
- goos:
- linux
- darwin
- windows
- darwin
goarch:
- amd64
- arm64
- riscv64
ldflags:
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
- -X github.com/kubescape/backend/pkg/versioncheck.Client={{.Env.CLIENT}}
hooks:
post:
- cmd: >
{{ if eq .Arch "amd64" }}
/bin/sh -lc 'sh build/goreleaser-post-e2e.sh'
{{ end }}
- id: downloader
dir: downloader
binary: downloader
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
- id: http
dir: httphandler
binary: ksserver
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
- -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
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
archives:
- format: binary
id: binaries
name_template: >-
{{ .Binary }}
- format: tar.gz
name_template: >-
{{ .Binary }}
changelog:
sort: asc
@@ -125,24 +50,11 @@ changelog:
- "^test:"
checksum:
name_template: "checksums.sha256"
ids:
- binaries
split: true
sboms:
- artifacts: binary
krews:
- name: kubescape
ids:
- cli
skip_upload: true
homepage: https://kubescape.io/
description: It includes risk analysis, security compliance, and misconfiguration scanning with an easy-to-use CLI interface, flexible output formats, and automated scanning capabilities.
short_description: Scan resources and cluster configs against security frameworks.
release:
draft: false
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
documents:
- "{{ .Binary }}.sbom"

View File

@@ -3,58 +3,40 @@ 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: linux
arch: amd64
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_linux_amd64.tar.gz" .TagName) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: linux
arch: arm64
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_linux_arm64.tar.gz" .TagName) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: darwin
arch: amd64
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_darwin_amd64.tar.gz" .TagName) .TagName }}
{{ 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 (printf "kubescape_%s_darwin_arm64.tar.gz" .TagName) .TagName }}
{{ 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 (printf "kubescape_%s_windows_amd64.tar.gz" .TagName) .TagName }}
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/{{ .TagName }}/kubescape-windows-latest.tar.gz" .TagName }}
bin: kubescape.exe
- selector:
matchLabels:
os: windows
arch: arm64
{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_windows_arm64.tar.gz" .TagName) .TagName }}
bin: kubescape.exe
shortDescription: Scan resources and cluster configs against security frameworks.
description: |
Kubescape is the first tool for testing if Kubernetes is deployed securely
according to mitigations and best practices. It includes risk analysis,
security compliance, and misconfiguration scanning with an easy-to-use
CLI interface, flexible output formats, and automated scanning capabilities.
Features:
- Risk analysis: Identify vulnerabilities and security risks in your cluster
- Security compliance: Check your cluster against multiple security frameworks
- Misconfiguration scanning: Detect security misconfigurations in your workloads
- Flexible output: Results in JSON, SARIF, HTML, JUnit, and Prometheus formats
- CI/CD integration: Easily integrate into your CI/CD pipeline
homepage: https://kubescape.io/
caveats: |
Requires kubectl and basic knowledge of Kubernetes.
Run 'kubescape scan' to scan your Kubernetes cluster or manifests.

View File

@@ -1,5 +1,24 @@
# Adopters
The Kubescape project manages this document in the central project repository.
# Well-known companies
Go to the [centralized ADOPTERS.md](https://github.com/kubescape/project-governance/blob/main/ADOPTERS.md)
List of well-known companies who are publicly acknowledge using and/or contributing to Kubescape are (in alphabetical order):
* AWS uses Kubescape in the security training material [link](https://catalog.workshops.aws/containersecurity/en-US/module2)
* Energi Danmark: Publicly talking about how they use Kubescape in their CI/CD pipeline [link](https://www.armosec.io/energi-danmark-business-support/)
* Gitpod: Used Kubescape in their SOC2 compliance process [link](https://www.armosec.io/gitpod/)
* Intel: using Kubescape for security prioritization [video](https://youtu.be/1iCW1KboypY?si=OjmnshWbpFNVPGJT)
* Orange Business: talking about Kubescape/ARMO service they are doing [video](https://www.youtube.com/watch?v=cbJYCUM8578)
* Rabobank: talked at KCD Amsterdam about having Kubescape in their technology stack [video](https://youtu.be/oa_YJmjwepI?si=vSrFW6seMKHj2Lze) [image](/docs/img/kcd-amsterdam-rabo.jpg)
* VMWare/Bitnami: listing Kubescape in their public image/helm repository [link](https://github.com/bitnami/containers/tree/main/bitnami/kubescape)
# Users
If you want to be listed here and share with others your experience, open a PR and add the bellow table:
| Name | Company | Use case | Contact for questions (optional) |
| ---- | ------- | -------- | -------------------------------- |
| Yonathan Amzallag | ARMO | Vulnerability monitoring | yonatana@armosec.io |
| Engin Diri | Schwarz IT (SIT) | Ensure continuous compliance for edge k8s cluster | engin.diri@mail.schwarz |
| Idan Bidani | Cox Communications | Security analysis for k8s best practices in CI pipelines of 3,000 applications 🔒☸ | idan.bidani@cox.com |

View File

@@ -1,5 +1,3 @@
# Code of Conduct
## Code of Conduct
The Kubescape project manages this document in the central project repository.
Go to the [centralized CODE_OF_CONDUCT.md](https://github.com/kubescape/project-governance/blob/main/CODE_OF_CONDUCT.md)
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).

View File

@@ -1,5 +0,0 @@
# Community
The Kubescape project manages this document in the central project repository.
Go to the [centralized COMMUNITY.md](https://github.com/kubescape/project-governance/blob/main/COMMUNITY.md)

View File

@@ -1,5 +1,100 @@
# Contributing
The Kubescape project manages this document in the central project repository.
First, it is awesome that you are considering contributing to Kubescape! Contributing is important and fun and we welcome your efforts.
Go to the [centralized CONTRIBUTING.md](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md)
When contributing, we categorize contributions into two:
* Small code changes or fixes, whose scope is limited to one or two files
* Complex features and improvements, with potentially unlimited scope
If you have a small change, feel free to fire up a Pull Request.
When planning a bigger change, please first discuss the change you wish to make via an [issue](https://github.com/kubescape/kubescape/issues),
so the maintainers are able to help guide you and let you know if you are going in the right direction.
[You can also find the maintainers on the CNCF Slack](https://kubescape.io/project/community/#slack) or [join our bi-weekly project meeting](https://kubescape.io/project/community/#community-meetings).
## Code of Conduct
Please follow our [code of conduct](CODE_OF_CONDUCT.md) in all of your interactions within the project.
## Build and test locally
Please follow the [instructions here](https://github.com/kubescape/kubescape/wiki/Building).
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Open Pull Request to the `master` branch.
4. We will merge the Pull Request once you have the sign-off.
## Developer Certificate of Origin
All commits to the project must be "signed off", which states that you agree to the terms of the [Developer Certificate of Origin](https://developercertificate.org/). This is done by adding a "Signed-off-by:" line in the commit message, with your name and email address.
Commits made through the GitHub web application are automatically signed off.
### Configuring Git to sign off commits
First, configure your name and email address in Git global settings:
```
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
```
You can now sign off per-commit, or configure Git to always sign off commits per repository.
### Sign off per-commit
Add [`-s`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s) to your Git command line. For example:
```git commit -s -m "Fix issue 64738"```
This is tedious, and if you forget, you'll have to [amend your commit](#fixing-a-commit-where-the-dco-failed).
### Configure a repository to always include sign off
There are many ways to achieve this with Git hooks, but the simplest is to do the following:
```
cd your-repo
curl -Ls https://gist.githubusercontent.com/dixudx/7d7edea35b4d91e1a2a8fbf41d0954fa/raw/prepare-commit-msg -o .git/hooks/prepare-commit-msg
chmod +x .git/hooks/prepare-commit-msg
```
### Use semantic commit messages (optional)
When contributing, you could consider using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), in order to improve logs readability and help us to automatically generate `CHANGELOG`s.
Format: `<type>(<scope>): <subject>`
`<scope>` is optional
#### Example
```
feat(cmd): add kubectl plugin
^--^ ^-^ ^----------------^
| | |
| | +-> subject: summary in present tense.
| |
| +-------> scope: point of interest
|
+-------> type: chore, docs, feat, fix, refactor, style, or test.
```
More Examples:
* `feat`: new feature for the user, not a new feature for build script
* `fix`: bug fix for the user, not a fix to a build script
* `docs`: changes to the documentation
* `style`: formatting, missing semi colons, etc; no production code change
* `refactor`: refactoring production code, eg. renaming a variable
* `test`: adding missing tests, refactoring tests; no production code change
* `chore`: updating grunt tasks etc; no production code change
## Fixing a commit where the DCO failed
Check out [this guide](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md).

View File

@@ -1,5 +1,60 @@
# Governance
# Governance of Kubescape
The Kubescape project manages this document in the central project repository.
## Overview
Go to the [centralized GOVERNANCE.md](https://github.com/kubescape/project-governance/blob/main/GOVERNANCE.md)
The Kubescape project is an open-source initiative dedicated to improve security and best practices in Kubernetes environments. This document outlines the governance structure of the Kubescape project and provides guidance for its community contributors.
## Code of Conduct
The CNCF has defined its core values and norms in a [code of conduct](CODE_OF_CONDUCT.md). As a CNCF project, we require all participants in the Kubescape community to behave in line with the standards set out in that document.
## Contributor roles
### Community Members
- Contribute to the project in any form.
- Participate in discussions and provide feedback.
Anyone can become a community member by contributing to the project. This can be in the form of code contributions, documentation, or any other form of project support.
### Committers
Committers are community members who have made significant and consistent contributions to the project. They have the ability to merge minor pull requests if assigned by maintainers.
- Review and merge minor pull requests.
- Assist maintainers in project tasks.
- Promote best practices within the community.
A contributor can be proposed as a committer by any existing maintainer. The proposal will be reviewed and voted on by the existing maintainers.
### Maintainers
Maintainers are responsible for the smooth operation of the project. They review and merge pull requests, manage releases, and ensure the quality and stability of the codebase.
- Ensure the quality and stability of the project.
- Resolve conflicts.
- Provide direction and set priorities for the project.
Maintainers are chosen based on their ongoing contributions and their demonstrated commitment to the project.
- Any committer who had at least 5 code contribution in the past 12 month can submit themselves to join the maintainer team. The maintainers will appoint members by a majority vote.
- Maintainers who have not taken part in project work (code, reviews, discussions) in the past 12 months will be considered inactive, and may be removed from the maintainer team.
## Processes
### Proposing Changes
1. Open an issue on the project repository to discuss the proposed change.
2. Once there is consensus around the proposed change, create a pull request.
3. Pull requests will be reviewed by committers and/or maintainers.
4. Once the pull request has received approval, it can be merged into the main codebase.
### Conflict Resolution
1. In case of any conflicts, it is primarily the responsibility of the parties involved to resolve it.
2. If the conflict cannot be resolved, it will be escalated to the maintainers for resolution.
3. Maintainers' decision will be final in case of unresolved conflicts.
## Changes to the Governance Document
Proposed changes to this governance document should follow the same process as any other code change to the Kubescape project (see "Proposing Changes").

View File

@@ -1,273 +0,0 @@
# Krew Release Automation Guide
This document explains how kubescape automates publishing to the Kubernetes plugin package manager, krew.
## What is Krew?
Krew is a plugin manager for `kubectl`. It allows users to discover and install `kubectl` plugins easily. You can learn more about krew at [https://krew.sigs.k8s.io/](https://krew.sigs.k8s.io/).
## How kubescape publishes to krew
We use the [krew-release-bot](https://github.com/rajatjindal/krew-release-bot) to automatically create pull requests to the [kubernetes-sigs/krew-index](https://github.com/kubernetes-sigs/krew-index) repository whenever a new release of kubescape is published.
### Setup Overview
The automation consists of three components:
1. **`.krew.yaml`** - A template file that the bot uses to generate the krew plugin manifest
2. **`.github/workflows/02-release.yaml`** - GitHub Actions workflow that runs the krew-release-bot after a successful release
3. **`.goreleaser.yaml`** - GoReleaser configuration that defines the krew manifest (though upload is skipped)
### Why Use krew-release-bot Instead of GoReleaser's Built-in Krew Support?
You might have noticed that **GoReleaser has built-in krew support** in its `krews` section. However, almost all projects (including stern) use `skip_upload: true` and rely on **krew-release-bot** instead. Here's why:
#### Problems with GoReleaser's Built-in Krew Publishing
To use GoReleaser's direct krew publishing, you would need to:
```yaml
krews:
- name: kubescape
skip_upload: false # Instead of true
repository:
owner: kubernetes-sigs
name: krew-index
token: "{{ .Env.KREW_INDEX_TOKEN }}" # Required!
pull_request:
enabled: true # Requires GoReleaser Pro for cross-repo PRs
```
This approach has several critical issues:
1. **Permission Barrier**: Almost no one has write access to `kubernetes-sigs/krew-index`. You would need special permissions from the Krew maintainers, which is rarely granted.
2. **Security Risk**: You'd need to store a GitHub personal access token with write access to the krew-index in your repository secrets. This token could be compromised and used to make unauthorized changes to the krew-index.
3. **GoReleaser Pro Required**: To create pull requests to a different repository (cross-repository), you need GoReleaser Pro, which is a paid product.
4. **Manual Work**: Even if you had access, you'd need to manually configure and maintain the repository settings, tokens, and potentially deal with rate limits and authentication issues.
#### Why krew-release-bot is the Right Solution
The **krew-release-bot** was created by the Kubernetes community (in collaboration with the Krew team) specifically to solve these problems:
- **No Repository Access Required**: The bot acts as an intermediary with pre-configured access to krew-index. You don't need write permissions.
- **No Tokens Needed**: It uses GitHub's `GITHUB_TOKEN` (automatically available in GitHub Actions) via webhooks and events. No personal access tokens required.
- **Designed for Krew**: It's specifically built for the krew-index workflow and integrates with Krew's automation.
- **Automatic Merging**: The Krew team has configured their CI to automatically test and merge PRs from krew-release-bot (usually within 5-10 minutes).
- **Officially Recommended**: The Krew team explicitly recommends this approach in their documentation as the standard way to automate plugin updates.
- **Free and Open Source**: No paid subscriptions required.
#### The Real-World Evidence
Looking at recent pull requests to `kubernetes-sigs/krew-index`, **almost all automated plugin updates are created by krew-release-bot**. You'll see patterns like:
```
Author: krew-release-bot
Title: "release new version v0.6.11 of radar"
```
This demonstrates that the entire Kubernetes ecosystem has standardized on krew-release-bot, not GoReleaser's built-in publishing.
#### Summary
While GoReleaser's built-in krew support exists in the code, it's **practically unusable for the krew-index repository** due to permission and security constraints. The krew-release-bot is the de facto standard because:
- It works without special permissions
- It's more secure
- It integrates with Krew's automation
- It's free and recommended by the Krew team
This is why we use `skip_upload: true` in GoReleaser and let krew-release-bot handle the actual publishing.
### The Template File
The `.krew.yaml` file in the repository root is a Go template that contains placeholders for dynamic values:
```yaml
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: kubescape
spec:
version: {{ .TagName }}
platforms:
- selector:
matchLabels:
os: linux
arch: amd64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_linux_amd64.tar.gz" $version) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: linux
arch: arm64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_linux_arm64.tar.gz" $version) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: darwin
arch: amd64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_darwin_amd64.tar.gz" $version) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: darwin
arch: arm64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_darwin_arm64.tar.gz" $version) .TagName }}
bin: kubescape
- selector:
matchLabels:
os: windows
arch: amd64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_windows_amd64.tar.gz" $version) .TagName }}
bin: kubescape.exe
- selector:
matchLabels:
os: windows
arch: arm64
{{ $version := trimPrefix "v" .TagName }}{{ addURIAndSha "https://github.com/kubescape/kubescape/releases/download/" .TagName (printf "kubescape_%s_windows_arm64.tar.gz" $version) .TagName }}
bin: kubescape.exe
shortDescription: Scan resources and cluster configs against security frameworks.
description: |
Kubescape is the first tool for testing if Kubernetes is deployed securely
according to mitigations and best practices. It includes risk analysis,
security compliance, and misconfiguration scanning with an easy-to-use
CLI interface, flexible output formats, and automated scanning capabilities.
Features:
- Risk analysis: Identify vulnerabilities and security risks in your cluster
- Security compliance: Check your cluster against multiple security frameworks
- Misconfiguration scanning: Detect security misconfigurations in your workloads
- Flexible output: Results in JSON, SARIF, HTML, JUnit, and Prometheus formats
- CI/CD integration: Easily integrate into your CI/CD pipeline
homepage: https://kubescape.io/
caveats: |
Requires kubectl and basic knowledge of Kubernetes.
Run 'kubescape scan' to scan your Kubernetes cluster or manifests.
```
The `{{ .TagName }}` is replaced with the release tag (e.g., `v3.0.0`), `{{ trimPrefix "v" .TagName }}` removes the version prefix, and `{{ addURIAndSha ... }}` calculates the SHA256 checksum for the binary archive.
### Release Workflow
The release workflow (`.github/workflows/02-release.yaml`) can be triggered in two ways:
1. **Automatic**: When a new tag matching the pattern `v[0-9]+.[0-9]+.[0-9]+` is pushed to the repository
2. **Manual**: Via `workflow_dispatch` with an optional `skip_publish` input
When the workflow is triggered:
1. GoReleaser builds and publishes the release artifacts (unless `skip_publish=true` is set)
2. The krew-release-bot step runs conditionally:
- It **runs** when triggered by a tag push OR by `workflow_dispatch` with `skip_publish=false`
- It **skips** when triggered by `workflow_dispatch` with `skip_publish=true` (default)
3. When it runs, the bot:
- Reads the `.krew.yaml` template
- Fills in the template with release information
- Creates a pull request to the `kubernetes-sigs/krew-index` repository
- The PR is automatically tested and merged by krew's infrastructure
### Workflow Permissions
The release job has the following permissions:
```yaml
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
```
These permissions are necessary for GoReleaser to create releases and upload artifacts.
### Testing the Template
Before committing changes to `.krew.yaml`, you can test how the template will be rendered using Docker:
```bash
docker run -v $(pwd)/.krew.yaml:/tmp/.krew.yaml ghcr.io/rajatjindal/krew-release-bot:v0.0.47 \
krew-release-bot template --tag v3.0.0 --template-file /tmp/.krew.yaml
```
This will output the generated krew manifest file, allowing you to verify:
- The version field is correct
- All download URLs are properly formatted
- The SHA256 checksum will be calculated correctly
### Why skip_upload in GoReleaser?
In `.goreleaser.yaml`, the `krews` section has `skip_upload: true`:
```yaml
krews:
- name: kubescape
ids:
- cli
skip_upload: true # We use krew-release-bot instead
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.
```
This is intentional because:
- GoReleaser generates the manifest but doesn't have built-in support for submitting PRs to krew-index
- krew-release-bot is the recommended tool for krew automation by the Krew team
- Using krew-release-bot provides automatic testing and merging of version bump PRs
### Manual Release Testing
You can test the release workflow manually without publishing to krew by using `workflow_dispatch`:
1. Go to Actions tab in GitHub
2. Select "02-create_release" workflow
3. Click "Run workflow"
4. The `skip_publish` input defaults to `true` (publishing will be skipped)
5. Set `skip_publish` to `false` if you want to test the full release process including krew indexing
### Making Changes to the Template
If you need to update the krew manifest (e.g., change the description, add platforms, or update the binary location):
1. Edit the `.krew.yaml` file
2. Test your changes with the Docker command shown above
3. Commit and push the changes
4. The next release will use the updated template
### Installing kubescape via krew
Once the plugin is indexed in krew, users can install it with:
```bash
kubectl krew install kubernetes-sigs/kubescape
```
Or after index update:
```bash
kubectl krew install kubescape
```
### Further Reading
- [Krew official documentation](https://krew.sigs.k8s.io/docs/developer-guide/)
- [krew-release-bot repository](https://github.com/rajatjindal/krew-release-bot)
- [Krew plugin submission guide](https://krew.sigs.k8s.io/docs/developer-guide/develop/plugins/)

View File

@@ -1,5 +1,12 @@
# Maintainers
The Kubescape project manages this document in the central project repository.
The following table lists the Kubescape project core maintainers:
Go to the [centralized MAINTAINERS.md](https://github.com/kubescape/project-governance/blob/main/MAINTAINERS.md)
| Name | GitHub | Organization
| --- | --- | ---
| [Matthias Bertschy](https://www.linkedin.com/in/matthias-bertschy-b427b815/) | [@matthyx](https://github.com/matthyx) | [ARMO](https://www.armosec.io/)
| [Craig Box](https://www.linkedin.com/in/crbnz/) | [@craigbox](https://github.com/craigbox) | [Solo.io](https://www.solo.io/)
| [Ben Hirschberg](https://www.linkedin.com/in/benyamin-ben-hirschberg-66141890) | [@slashben](https://github.com/slashben) | [ARMO](https://www.armosec.io/)
| [Rotem Refael](https://www.linkedin.com/in/rotem-refael) | [@rotemamsa](https://github.com/rotemamsa) | [ARMO](https://www.armosec.io/)
| [David Wertenteil](https://www.linkedin.com/in/david-wertenteil-0ba277b9) | [@dwertent](https://github.com/dwertent) | [Kaleido](https://kaleido.io/)
| [Amir Malka](https://www.linkedin.com/in/amirmalka) | [@amirmalka](https://github.com/amirmalka) | [ARMO](https://www.armosec.io/)

484
README.md
View File

@@ -3,12 +3,11 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/kubescape/kubescape)](https://goreportcard.com/report/github.com/kubescape/kubescape)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/kubescape/kubescape)
[![GitHub](https://img.shields.io/github/license/kubescape/kubescape)](https://github.com/kubescape/kubescape/blob/master/LICENSE)
[![CNCF](https://shields.io/badge/CNCF-Incubating%20project-blue?logo=linux-foundation&style=flat)](https://landscape.cncf.io/?item=provisioning--security-compliance--kubescape)
[![CNCF](https://shields.io/badge/CNCF-Sandbox%20project-blue?logo=linux-foundation&style=flat)](https://landscape.cncf.io/card-mode?project=sandbox&selected=kubescape)
[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kubescape)](https://artifacthub.io/packages/search?repo=kubescape)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkubescape%2Fkubescape.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkubescape%2Fkubescape?ref=badge_shield&issueType=license)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/6944/badge)](https://www.bestpractices.dev/projects/6944)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/kubescape/kubescape/badge)](https://securityscorecards.dev/viewer/?uri=github.com/kubescape/kubescape)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen?logo=gitbook)](https://kubescape.io/docs/)
[![Stars](https://img.shields.io/github/stars/kubescape/kubescape?style=social)](https://github.com/kubescape/kubescape/stargazers)
[![Twitter Follow](https://img.shields.io/twitter/follow/kubescape?style=social)](https://twitter.com/kubescape)
[![Slack](https://img.shields.io/badge/slack-kubescape-blueviolet?logo=slack)](https://cloud-native.slack.com/archives/C04EY3ZF9GE)
@@ -23,478 +22,119 @@
_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.
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/).
**Key features of Kubescape include**
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape!_
* **Shift-left security**: Kubescape enables developers to scan for misconfigurations as early as the manifest file submission stage, promoting a proactive approach to security.
* **IDE and CI/CD integration**: The tool integrates seamlessly with popular IDEs like VSCode and Lens, as well as CI/CD platforms such as GitHub and GitLab, allowing for security checks throughout the development process.
* **Cluster scanning**: Kubescape can scan active Kubernetes clusters for vulnerabilities, misconfigurations, and security issues
* **Multiple framework support**: Kubescape can test against various security frameworks, including NSA, MITRE, SOC2, and more.
* **YAML and Helm chart validation**: The tool checks YAML files and Helm charts for correct configuration according to the frameworks above, without requiring an active cluster.
* **Kubernetes hardening**: Kubescape ensures proactive identification and rapid remediation of misconfigurations and vulnerabilities through manual, recurring, or event-triggered scans.
* **Runtime security**: Kubescape extends its protection to the runtime environment, providing continuous monitoring and threat detection for deployed applications.
* **Compliance management**: The tool aids in maintaining compliance with recognized frameworks and standards, simplifying the process of meeting regulatory requirements.
* **Multi-cloud support**: Kubescape offers frictionless security across various cloud providers and Kubernetes distributions.
---
By providing this comprehensive security coverage from development to production, Kubescape enables organizations to implement a robust security posture throughout their Kubernetes deployment, addressing potential vulnerabilities and threats at every stage of the application lifecycle.
## 📑 Table of Contents
Kubescape was created by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository) and is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/).
- [Features](#-features)
- [Demo](#-demo)
- [Quick Start](#-quick-start)
- [Installation](#-installation)
- [CLI Commands](#%EF%B8%8F-cli-commands)
- [Usage Examples](#-usage-examples)
- [Architecture](#%EF%B8%8F-architecture)
- [In-Cluster Operator](#%EF%B8%8F-in-cluster-operator)
- [Integrations](#-integrations)
- [Community](#-community)
- [Changelog](#changelog)
- [License](#license)
_Please [star ⭐](https://github.com/kubescape/kubescape/stargazers) the repo if you want us to continue developing and improving Kubescape! 😀_
---
## Demo
## ✨ Features
Kubescape has a command line tool that you can use to quickly get a report on the security posture of a Kubernetes cluster:
| 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 |
<img src="docs/img/demo-v3.gif">
---
## Getting started
## 🎬 Demo
<img src="docs/img/demo-v3.gif" alt="Kubescape CLI demo">
---
## 🚀 Quick Start
### 1. Install Kubescape
Experimenting with Kubescape is as easy as:
```sh
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
```
> 💡 See [Installation](#-installation) for more options (Homebrew, Krew, Windows, etc.)
This script will automatically download the latest Kubescape CLI release and scan the Kubernetes cluster in your current kubectl context.
### 2. Run Your First Scan
Learn more about:
```sh
# Scan your current cluster
kubescape 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/)
# 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.
_Did you know you can use Kubescape in all these places?_
<div align="center">
<img src="docs/img/ks-cli-arch.png" width="600" alt="CLI Architecture">
<img src="docs/img/ksfromcodetodeploy.png" alt="Places you can use Kubescape: in your IDE, CI, CD, or against a running cluster.">
</div>
**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
### Continuous security monitoring with the Kubescape Operator
### Operator Mode (In-Cluster)
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/).
For continuous monitoring, deploy the Kubescape operator via Helm.
### Using Kubescape as a GitHub Action
<div align="center">
<img src="docs/img/ks-operator-arch.png" width="600" alt="Operator Architecture">
</div>
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).
**Additional Capabilities:**
- Continuous configuration scanning
- Image vulnerability scanning
- Runtime analysis with eBPF
- Network policy generation
## Under the hood
📖 **[Full Architecture Documentation →](docs/architecture.md)**
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). Kubescape retrieves Kubernetes resources from the API server and runs a set of [Rego snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io?utm_source=github&utm_medium=repository).
---
Container image scanning is powered by [Grype](https://github.com/anchore/grype) and image patching uses [Copacetic](https://github.com/project-copacetic/copacetic).
## ☸️ In-Cluster Operator
By default, CLI scan results are printed in a console-friendly manner, but they can be:
The Kubescape operator provides continuous security monitoring in your cluster:
* exported to JSON, junit XML or SARIF
* rendered to HTML or PDF
* submitted to a [cloud service](docs/providers.md)
```bash
# Add the Kubescape Helm repository
helm repo add kubescape https://kubescape.github.io/helm-charts/
### In-cluster architecture
# Install the operator
helm upgrade --install kubescape kubescape/kubescape-operator \
--namespace kubescape \
--create-namespace
```
![Architecture diagram](docs/img/architecture-diagram.png)
**Operator Features:**
- 🔄 Continuous misconfiguration scanning
- 🐳 Image vulnerability scanning for all workloads
- 🔍 Runtime threat detection (eBPF-based)
- 🌐 Network policy generation
- 📈 Prometheus metrics integration
## Community
📖 **[Operator Installation Guide →](https://kubescape.io/docs/operator/)**
We welcome user feedback and ideas for improvement.
---
Kubescape users and developers meet on the CNCF Slack. [Join it](https://slack.cncf.io/) and find us in [#kubescape](https://cloud-native.slack.com/archives/C04EY3ZF9GE) or [#kubescape-dev](https://cloud-native.slack.com/archives/C04GY6H082K).
## 🔌 Integrations
We hold [community meetings](https://zoom.us/j/95174063585) on Zoom, every second Tuesday, at 15:00 CET. ([See that in your local time zone](https://time.is/compare/1500_in_CET).
### CI/CD
* Meetings are announced in [#kubescape-dev](https://cloud-native.slack.com/archives/C04GY6H082K) on Slack (including any cancellations).
* [The agenda and notes are in a public Google doc](https://docs.google.com/document/d/1X_eyhPzJvb4ascVQ2e0jN87LAvq7lTuXT5d4gQxi8us/edit?tab=t.0).
* [Recordings are posted to YouTube](https://www.youtube.com/@kubescape).
| 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/) |
The Kubescape project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
### IDE Extensions
### Adopters
| IDE | Extension |
|-----|-----------|
| **VS Code** | [Kubescape Extension](https://marketplace.visualstudio.com/items?itemName=kubescape.kubescape) |
| **Lens** | [Kubescape Lens Extension](https://github.com/armosec/lens-kubescape) |
See [here](ADOPTERS.md) for a list of reference adopters.
### Where You Can Use Kubescape
### Contributions
<div align="center">
<img src="docs/img/ksfromcodetodeploy.png" alt="Kubescape integration points: IDE, CI, CD, Runtime">
</div>
Thanks to all our contributors! Check out our [CONTRIBUTING](CONTRIBUTING.md) file to learn how to join them.
---
* Feel free to pick a task from the [issues](https://github.com/kubescape/kubescape/issues?q=is%3Aissue+is%3Aopen+label%3A%22open+for+contribution%22), [roadmap](docs/roadmap.md) or suggest a feature of your own.
* [Open an issue](https://github.com/kubescape/kubescape/issues/new/choose): we aim to respond to all issues within 48 hours.
## 👥 Community
<br>
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 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 [releases page](https://github.com/kubescape/kubescape/releases).
---
Kubescape changes are tracked on the [release](https://github.com/kubescape/kubescape/releases) page.
## License
Copyright 2021-2025, the Kubescape Authors. All rights reserved.
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.
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).
Kubescape is a [Cloud Native Computing Foundation (CNCF) sandbox project](https://www.cncf.io/sandbox-projects/) and was contributed by [ARMO](https://www.armosec.io/?utm_source=github&utm_medium=repository).
<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>
<img src="https://raw.githubusercontent.com/cncf/artwork/master/other/cncf-sandbox/horizontal/color/cncf-sandbox-horizontal-color.svg" width="300" alt="CNCF Sandbox Project">
</div>

View File

@@ -4,19 +4,15 @@ header:
last-reviewed: '2023-10-12'
expiration-date: '2024-10-12T01:00:00.000Z'
project-url: https://github.com/kubescape/kubescape/
project-release: 1.0.0
project-release: '1.0.0'
project-lifecycle:
status: active
bug-fixes-only: false
core-maintainers:
- github:amirmalka
- github:amitschendel
- github:bezbran
- github:craigbox
- github:dwertent
- github:matthyx
- github:rotemamsa
- github:slashben
- github:craigbox
- github:matthyx
- github:dwertent
contribution-policy:
accepts-pull-requests: true
accepts-automated-pull-requests: false

View File

@@ -1,5 +1,7 @@
# Security
# Reporting Security Issues
The Kubescape project manages this document in the central project repository.
To report a security issue or vulnerability, submit a [private vulnerability report via GitHub](https://github.com/kubescape/kubescape/security/advisories/new) to the repository maintainers with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
Go to the [centralized SECURITY.md](https://github.com/kubescape/project-governance/blob/main/SECURITY.md)
The maintainers will respond within 7 working days of your report. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline.
Other contacts: cncf-kubescape-maintainers@lists.cncf.io

View File

@@ -1,12 +1,20 @@
FROM gcr.io/distroless/static-debian13:nonroot
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 .
FROM gcr.io/distroless/static-debian12:nonroot
USER nonroot
WORKDIR /home/nonroot/
ARG TARGETPLATFORM
COPY $TARGETPLATFORM/downloader /usr/bin/downloader
RUN ["downloader"]
COPY $TARGETPLATFORM/ksserver /usr/bin/ksserver
COPY --from=builder /out/ksserver /usr/bin/ksserver
ARG image_version client
ENV RELEASE=$image_version CLIENT=$client

View File

@@ -1,241 +1,19 @@
# Building Kubescape
## Docker Build
This guide covers how to build Kubescape from source.
### Build your own Docker image
## 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
1. Clone Project
```
git clone https://github.com/kubescape/kubescape.git kubescape && cd "$_"
```
---
## Building the CLI
### Clone the Repository
```bash
git clone https://github.com/kubescape/kubescape.git
cd kubescape
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 .
```
### Build with Make
```bash
# Build for your current platform
make build
# The binary will be at ./kubescape
./kubescape version
3. Build kubescape Docker image
```
### Build Directly with Go
```bash
go build -o kubescape .
docker buildx build -t kubescape -f build/Dockerfile --load .
```
### 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)

View File

@@ -1,151 +0,0 @@
#!/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
RELEASE="${RELEASE:-}" "$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

View File

@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian13:debug-nonroot
FROM gcr.io/distroless/base-debian12:debug-nonroot
USER nonroot
WORKDIR /home/nonroot/
@@ -6,8 +6,7 @@ WORKDIR /home/nonroot/
ARG image_version client TARGETARCH
ENV RELEASE=$image_version CLIENT=$client
ARG TARGETPLATFORM
COPY $TARGETPLATFORM/kubescape /usr/bin/kubescape
COPY kubescape-${TARGETARCH}-ubuntu-latest /usr/bin/kubescape
RUN ["kubescape", "download", "artifacts"]
ENTRYPOINT ["kubescape"]

View File

@@ -1,6 +1,8 @@
package config
import (
"context"
"github.com/kubescape/go-logger"
"github.com/kubescape/kubescape/v3/core/meta"
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
@@ -13,7 +15,7 @@ func getDeleteCmd(ks meta.IKubescape) *cobra.Command {
Short: "Delete cached configurations",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
if err := ks.DeleteCachedConfig(&v1.DeleteConfig{}); err != nil {
if err := ks.DeleteCachedConfig(context.TODO(), &v1.DeleteConfig{}); err != nil {
logger.L().Fatal(err.Error())
}
},

View File

@@ -1,9 +1,9 @@
package download
import (
"context"
"fmt"
"path/filepath"
"slices"
"strings"
"github.com/kubescape/go-logger"
@@ -12,6 +12,7 @@ import (
"github.com/kubescape/kubescape/v3/core/meta"
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)
var (
@@ -73,9 +74,11 @@ func GetDownloadCmd(ks meta.IKubescape) *cobra.Command {
downloadInfo.Target = args[0]
if len(args) >= 2 {
downloadInfo.Identifier = args[1]
}
if err := ks.Download(&downloadInfo); err != nil {
if err := ks.Download(context.TODO(), &downloadInfo); err != nil {
logger.L().Fatal(err.Error())
}
return nil

View File

@@ -1,12 +1,14 @@
package fix
import (
"context"
"errors"
"fmt"
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/meta"
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
"github.com/spf13/cobra"
)
@@ -34,7 +36,7 @@ func GetFixCmd(ks meta.IKubescape) *cobra.Command {
}
fixInfo.ReportFile = args[0]
return ks.Fix(&fixInfo)
return ks.Fix(context.TODO(), &fixInfo)
},
}

View File

@@ -1,9 +1,9 @@
package list
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"github.com/kubescape/go-logger"
@@ -12,6 +12,7 @@ import (
"github.com/kubescape/kubescape/v3/core/meta"
v1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)
var (
@@ -26,7 +27,7 @@ var (
%[1]s list controls
Control documentation:
https://kubescape.io/docs/controls/
https://hub.armosec.io/docs/controls
`, cautils.ExecName())
)
@@ -61,7 +62,7 @@ func GetListCmd(ks meta.IKubescape) *cobra.Command {
listPolicies.Target = args[0]
if err := ks.List(&listPolicies); err != nil {
if err := ks.List(context.TODO(), &listPolicies); err != nil {
logger.L().Fatal(err.Error())
}
return nil

View File

@@ -1,499 +0,0 @@
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 := ""
switch level {
case "workload":
labelSelector = "kubescape.io/context=filtered"
case "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"
}
namespaceStr, ok := namespace.(string)
if !ok {
return nil, fmt.Errorf("namespace must be a string")
}
manifestName, ok := arguments["manifest_name"]
if !ok {
return nil, fmt.Errorf("manifest_name is required")
}
manifestNameStr, ok := manifestName.(string)
if !ok {
return nil, fmt.Errorf("manifest_name must be a string")
}
manifest, err := ksServer.ksClient.VulnerabilityManifests(namespaceStr).Get(context.Background(), manifestNameStr, 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"
}
namespaceStr, ok := namespace.(string)
if !ok {
return nil, fmt.Errorf("namespace must be a string")
}
manifestName, ok := arguments["manifest_name"]
if !ok {
return nil, fmt.Errorf("manifest_name is required")
}
manifestNameStr, ok := manifestName.(string)
if !ok {
return nil, fmt.Errorf("manifest_name must be a string")
}
cveID, ok := arguments["cve_id"]
if !ok {
return nil, fmt.Errorf("cve_id is required")
}
cveIDStr, ok := cveID.(string)
if !ok {
return nil, fmt.Errorf("cve_id must be a string")
}
manifest, err := ksServer.ksClient.VulnerabilityManifests(namespaceStr).Get(context.Background(), manifestNameStr, 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 == cveIDStr {
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"
}
namespaceStr, ok := namespace.(string)
if !ok {
return nil, fmt.Errorf("namespace must be a string")
}
manifests, err := ksServer.ksClient.WorkloadConfigurationScans(namespaceStr).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"
}
namespaceStr, ok := namespace.(string)
if !ok {
return nil, fmt.Errorf("namespace must be a string")
}
manifestName, ok := arguments["manifest_name"]
if !ok {
return nil, fmt.Errorf("manifest_name is required")
}
manifestNameStr, ok := manifestName.(string)
if !ok {
return nil, fmt.Errorf("manifest_name must be a string")
}
manifest, err := ksServer.ksClient.WorkloadConfigurationScans(namespaceStr).Get(context.Background(), manifestNameStr, 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", 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
}

View File

@@ -1,14 +0,0 @@
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)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/meta"
"github.com/spf13/cobra"
)
@@ -14,7 +15,7 @@ const (
)
var operatorExamples = fmt.Sprintf(`
# Trigger a configuration scan
%[1]s operator scan configurations
@@ -34,16 +35,16 @@ func GetOperatorCmd(ks meta.IKubescape) *cobra.Command {
Args: func(cmd *cobra.Command, args []string) error {
operatorInfo.Subcommands = append(operatorInfo.Subcommands, "operator")
if len(args) < 2 {
return errors.New("for the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above")
return errors.New("For the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above.")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return errors.New("for the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above")
return errors.New("For the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above.")
}
if args[0] != scanSubCommand {
return fmt.Errorf("for the operator sub-command, only %s is supported. Refer to the examples above", scanSubCommand)
return errors.New(fmt.Sprintf("For the operator sub-command, only %s is supported. Refer to the examples above.", scanSubCommand))
}
return nil
},

View File

@@ -21,7 +21,7 @@ func TestGetOperatorCmd(t *testing.T) {
assert.Equal(t, operatorExamples, cmd.Example)
err := cmd.Args(&cobra.Command{}, []string{})
expectedErrorMessage := "for the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above"
expectedErrorMessage := "For the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above."
assert.Equal(t, expectedErrorMessage, err.Error())
err = cmd.Args(&cobra.Command{}, []string{"scan", "configurations"})
@@ -37,6 +37,6 @@ func TestGetOperatorCmd(t *testing.T) {
assert.Equal(t, expectedErrorMessage, err.Error())
err = cmd.RunE(&cobra.Command{}, []string{"random-subcommand", "random-config"})
expectedErrorMessage = "for the operator sub-command, only " + scanSubCommand + " is supported. Refer to the examples above"
expectedErrorMessage = "For the operator sub-command, only " + scanSubCommand + " is supported. Refer to the examples above."
assert.Equal(t, expectedErrorMessage, err.Error())
}

View File

@@ -32,7 +32,7 @@ func getOperatorScanCmd(ks meta.IKubescape, operatorInfo cautils.OperatorInfo) *
return errors.New("for operator scan sub command, you must pass at least 1 more sub commands, see above examples")
}
if (args[0] != vulnerabilitiesSubCommand) && (args[0] != configurationsSubCommand) {
return fmt.Errorf("for the operator sub-command, only %s and %s are supported. Refer to the examples above", vulnerabilitiesSubCommand, configurationsSubCommand)
return errors.New(fmt.Sprintf("For the operator sub-command, only %s and %s are supported. Refer to the examples above.", vulnerabilitiesSubCommand, configurationsSubCommand))
}
return nil
},

View File

@@ -41,6 +41,6 @@ func TestGetOperatorScanCmd(t *testing.T) {
assert.Nil(t, err)
err = cmd.RunE(&cobra.Command{}, []string{"random"})
expectedErrorMessage = "for the operator sub-command, only " + vulnerabilitiesSubCommand + " and " + configurationsSubCommand + " are supported. Refer to the examples above"
expectedErrorMessage = "For the operator sub-command, only " + vulnerabilitiesSubCommand + " and " + configurationsSubCommand + " are supported. Refer to the examples above."
assert.Equal(t, expectedErrorMessage, err.Error())
}

View File

@@ -73,7 +73,7 @@ We will demonstrate how to use the patch command with an example of [nginx](http
sudo buildkitd
```
2. In a separate terminal, run the `kubescape patch` command:
2. In a seperate terminal, run the `kubescape patch` command:
```bash
sudo kubescape patch --image docker.io/library/nginx:1.22

View File

@@ -1,17 +1,21 @@
package patch
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/distribution/reference"
"github.com/docker/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"
)
@@ -27,7 +31,6 @@ 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,15 +52,12 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
return err
}
// Set the UseDefaultMatchers field in scanInfo
scanInfo.UseDefaultMatchers = useDefaultMatchers
exceedsSeverityThreshold, err := ks.Patch(&patchInfo, &scanInfo)
results, err := ks.Patch(context.Background(), &patchInfo, &scanInfo)
if err != nil {
return err
}
if exceedsSeverityThreshold {
if imagescan.ExceedsSeverityThreshold(results, imagescan.ParseSeverity(scanInfo.FailThresholdSeverity)) {
shared.TerminateOnExceedingSeverity(&scanInfo, logger.L())
}
@@ -79,7 +79,6 @@ 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
}

View File

@@ -3,8 +3,6 @@ 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"
@@ -52,18 +50,3 @@ 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)
}

View File

@@ -1,51 +0,0 @@
package prerequisites
import (
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v3/core/meta"
"github.com/kubescape/sizing-checker/pkg/checks/connectivitycheck"
"github.com/kubescape/sizing-checker/pkg/checks/ebpfcheck"
"github.com/kubescape/sizing-checker/pkg/checks/pvcheck"
"github.com/kubescape/sizing-checker/pkg/checks/sizing"
"github.com/kubescape/sizing-checker/pkg/common"
"github.com/spf13/cobra"
)
func GetPreReqCmd(ks meta.IKubescape) *cobra.Command {
var kubeconfigPath *string
// preReqCmd represents the prerequisites command
preReqCmd := &cobra.Command{
Use: "prerequisites",
Short: "Check prerequisites for installing Kubescape Operator",
Run: func(cmd *cobra.Command, args []string) {
clientSet, inCluster := common.BuildKubeClient(*kubeconfigPath)
if clientSet == nil {
logger.L().Fatal("Could not create kube client. Exiting.")
}
// 1) Collect cluster data
clusterData, err := common.CollectClusterData(ks.Context(), clientSet)
if err != nil {
logger.L().Error("Failed to collect cluster data", helpers.Error(err))
}
// 2) Run checks
sizingResult := sizing.RunSizingChecker(clusterData)
pvResult := pvcheck.RunPVProvisioningCheck(ks.Context(), clientSet, clusterData, inCluster)
connectivityResult := connectivitycheck.RunConnectivityChecks(ks.Context(), clientSet, clusterData, inCluster)
ebpfResult := ebpfcheck.RunEbpfCheck(ks.Context(), clientSet, clusterData, inCluster)
// 3) Build and export the final ReportData
finalReport := common.BuildReportData(clusterData, sizingResult, pvResult, connectivityResult, ebpfResult)
finalReport.InCluster = inCluster
common.GenerateOutput(finalReport, inCluster)
},
}
kubeconfigPath = preReqCmd.PersistentFlags().String("kubeconfig", "", "Path to the kubeconfig file. If not set, in-cluster config is used or $HOME/.kube/config if outside a cluster.")
return preReqCmd
}

View File

@@ -1,7 +1,6 @@
package cmd
import (
"context"
"fmt"
"strings"
@@ -13,10 +12,8 @@ import (
"github.com/kubescape/kubescape/v3/cmd/download"
"github.com/kubescape/kubescape/v3/cmd/fix"
"github.com/kubescape/kubescape/v3/cmd/list"
"github.com/kubescape/kubescape/v3/cmd/mcpserver"
"github.com/kubescape/kubescape/v3/cmd/operator"
"github.com/kubescape/kubescape/v3/cmd/patch"
"github.com/kubescape/kubescape/v3/cmd/prerequisites"
"github.com/kubescape/kubescape/v3/cmd/scan"
"github.com/kubescape/kubescape/v3/cmd/update"
"github.com/kubescape/kubescape/v3/cmd/vap"
@@ -25,6 +22,7 @@ import (
"github.com/kubescape/kubescape/v3/core/cautils/getter"
"github.com/kubescape/kubescape/v3/core/core"
"github.com/kubescape/kubescape/v3/core/meta"
"github.com/spf13/cobra"
)
@@ -44,16 +42,16 @@ var ksExamples = fmt.Sprintf(`
%[1]s config view
`, cautils.ExecName())
func NewDefaultKubescapeCommand(ctx context.Context, ksVersion, ksCommit, ksDate string) *cobra.Command {
ks := core.NewKubescape(ctx)
return getRootCmd(ks, ksVersion, ksCommit, ksDate)
func NewDefaultKubescapeCommand() *cobra.Command {
ks := core.NewKubescape()
return getRootCmd(ks)
}
func getRootCmd(ks meta.IKubescape, ksVersion, ksCommit, ksDate string) *cobra.Command {
func getRootCmd(ks meta.IKubescape) *cobra.Command {
rootCmd := &cobra.Command{
Use: "kubescape",
Short: "Kubescape is a tool for testing Kubernetes security posture. Docs: https://kubescape.io/docs/",
Short: "Kubescape is a tool for testing Kubernetes security posture. Docs: https://hub.armosec.io/docs",
Example: ksExamples,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
k8sinterface.SetClusterContextName(rootInfo.KubeContext)
@@ -86,6 +84,8 @@ func getRootCmd(ks meta.IKubescape, ksVersion, ksCommit, ksDate string) *cobra.C
rootCmd.PersistentFlags().StringVarP(&rootInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/")))
rootCmd.PersistentFlags().StringVar(&rootInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]")
rootCmd.PersistentFlags().BoolVarP(&rootInfo.DisableColor, "disable-color", "", false, "Disable color output for logging")
rootCmd.PersistentFlags().BoolVarP(&rootInfo.EnableColor, "enable-color", "", false, "Force enable color output for logging")
rootCmd.PersistentFlags().StringVarP(&rootInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context")
// Supported commands
@@ -93,15 +93,13 @@ func getRootCmd(ks meta.IKubescape, ksVersion, ksCommit, ksDate string) *cobra.C
rootCmd.AddCommand(download.GetDownloadCmd(ks))
rootCmd.AddCommand(list.GetListCmd(ks))
rootCmd.AddCommand(completion.GetCompletionCmd())
rootCmd.AddCommand(version.GetVersionCmd(ks, ksVersion, ksCommit, ksDate))
rootCmd.AddCommand(version.GetVersionCmd())
rootCmd.AddCommand(config.GetConfigCmd(ks))
rootCmd.AddCommand(update.GetUpdateCmd(ks))
rootCmd.AddCommand(update.GetUpdateCmd())
rootCmd.AddCommand(fix.GetFixCmd(ks))
rootCmd.AddCommand(patch.GetPatchCmd(ks))
rootCmd.AddCommand(vap.GetVapHelperCmd())
rootCmd.AddCommand(operator.GetOperatorCmd(ks))
rootCmd.AddCommand(prerequisites.GetPreReqCmd(ks))
rootCmd.AddCommand(mcpserver.GetMCPServerCmd())
// deprecated commands
rootCmd.AddCommand(&cobra.Command{
@@ -116,7 +114,7 @@ func getRootCmd(ks meta.IKubescape, ksVersion, ksCommit, ksDate string) *cobra.C
return rootCmd
}
func Execute(ctx context.Context, ksVersion, ksCommit, ksDate string) error {
ks := NewDefaultKubescapeCommand(ctx, ksVersion, ksCommit, ksDate)
func Execute() error {
ks := NewDefaultKubescapeCommand()
return ks.Execute()
}
}

View File

@@ -1,24 +0,0 @@
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())
}
})
}

View File

@@ -14,10 +14,14 @@ import (
"github.com/kubescape/go-logger/zaplogger"
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/cautils/getter"
"github.com/mattn/go-isatty"
)
func initLogger() {
logger.DisableColor(rootInfo.DisableColor)
logger.EnableColor(rootInfo.EnableColor)
if rootInfo.LoggerName == "" {
if l := os.Getenv("KS_LOGGER_NAME"); l != "" {
rootInfo.LoggerName = l
@@ -31,8 +35,8 @@ func initLogger() {
}
logger.InitLogger(rootInfo.LoggerName)
}
}
func initLoggerLevel() {
if rootInfo.Logger == helpers.InfoLevel.String() {
} else if l := os.Getenv("KS_LOGGER"); l != "" {

View File

@@ -1,17 +1,20 @@
package scan
import (
"context"
"fmt"
"io"
"os"
"strings"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v3/cmd/shared"
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/meta"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
"github.com/spf13/cobra"
)
@@ -29,7 +32,7 @@ var (
Run '%[1]s list controls' for the list of supported controls
Control documentation:
https://kubescape.io/docs/controls/
https://hub.armosec.io/docs/controls
`, cautils.ExecName())
)
@@ -95,11 +98,12 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman
return err
}
results, err := ks.Scan(scanInfo)
ctx := context.TODO()
results, err := ks.Scan(ctx, scanInfo)
if err != nil {
logger.L().Fatal(err.Error())
}
if err := results.HandleResults(ks.Context(), scanInfo); err != nil {
if err := results.HandleResults(ctx); err != nil {
logger.L().Fatal(err.Error())
}
if !scanInfo.VerboseMode {

View File

@@ -1,22 +1,25 @@
package scan
import (
"context"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
reporthandlingapis "github.com/kubescape/opa-utils/reporthandling/apis"
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
"golang.org/x/exp/slices"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v3/cmd/shared"
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/cautils/getter"
"github.com/kubescape/kubescape/v3/core/meta"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
reporthandlingapis "github.com/kubescape/opa-utils/reporthandling/apis"
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/spf13/cobra"
)
@@ -112,12 +115,13 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm
scanInfo.SetPolicyIdentifiers(frameworks, apisv1.KindFramework)
results, err := ks.Scan(scanInfo)
ctx := context.TODO()
results, err := ks.Scan(ctx, scanInfo)
if err != nil {
logger.L().Fatal(err.Error())
}
if err = results.HandleResults(ks.Context(), scanInfo); err != nil {
if err = results.HandleResults(ctx); err != nil {
logger.L().Fatal(err.Error())
}

View File

@@ -1,6 +1,7 @@
package scan
import (
"context"
"fmt"
"github.com/kubescape/go-logger"
@@ -8,6 +9,8 @@ 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"
)
@@ -22,18 +25,12 @@ var (
# Scan the 'nginx' image and see the full report
%[1]s scan image "nginx" -v
# Scan the 'nginx' image and use exceptions
%[1]s scan image "nginx" --exceptions exceptions.json
`, cautils.ExecName())
)
// getImageCmd returns the scan image command
func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command {
var imgCredentials shared.ImageCredentials
var exceptions string
var useDefaultMatchers bool
cmd := &cobra.Command{
Use: "image <image>:<tag> [flags]",
Short: "Scan an image for vulnerabilities",
@@ -54,19 +51,17 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
}
imgScanInfo := &metav1.ImageScanInfo{
Image: args[0],
Username: imgCredentials.Username,
Password: imgCredentials.Password,
Exceptions: exceptions,
UseDefaultMatchers: useDefaultMatchers,
Image: args[0],
Username: imgCredentials.Username,
Password: imgCredentials.Password,
}
exceedsSeverityThreshold, err := ks.ScanImage(imgScanInfo, scanInfo)
results, err := ks.ScanImage(context.Background(), imgScanInfo, scanInfo)
if err != nil {
return err
}
if exceedsSeverityThreshold {
if imagescan.ExceedsSeverityThreshold(results, imagescan.ParseSeverity(scanInfo.FailThresholdSeverity)) {
shared.TerminateOnExceedingSeverity(scanInfo, logger.L())
}
@@ -74,11 +69,8 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
},
}
// The exceptions flag
cmd.PersistentFlags().StringVarP(&exceptions, "exceptions", "", "", "Path to the exceptions file")
cmd.PersistentFlags().StringVarP(&imgCredentials.Username, "username", "u", "", "Username for registry login")
cmd.PersistentFlags().StringVarP(&imgCredentials.Password, "password", "p", "", "Password for registry login")
cmd.PersistentFlags().BoolVarP(&useDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false)")
return cmd
}

View File

@@ -1,6 +1,7 @@
package scan
import (
"context"
"flag"
"fmt"
"strings"
@@ -63,8 +64,6 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
},
}
scanInfo.TriggeredByCLI = true
scanCmd.PersistentFlags().StringVarP(&scanInfo.AccountID, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
scanCmd.PersistentFlags().StringVarP(&scanInfo.AccessKey, "access-key", "", "", "Kubescape SaaS access key. Default will load access key from cache")
scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal")
@@ -92,8 +91,6 @@ 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.")
@@ -136,12 +133,15 @@ func setSecurityViewScanInfo(args []string, scanInfo *cautils.ScanInfo) {
}
func securityScan(scanInfo cautils.ScanInfo, ks meta.IKubescape) error {
results, err := ks.Scan(&scanInfo)
ctx := context.TODO()
results, err := ks.Scan(ctx, &scanInfo)
if err != nil {
return err
}
if err = results.HandleResults(ks.Context(), &scanInfo); err != nil {
if err = results.HandleResults(ctx); err != nil {
return err
}

View File

@@ -2,19 +2,20 @@ package scan
import (
"context"
"os"
"reflect"
"testing"
"time"
"github.com/kubescape/go-logger/helpers"
"github.com/stretchr/testify/assert"
"github.com/kubescape/kubescape/v3/cmd/shared"
"github.com/kubescape/kubescape/v3/core/cautils"
"github.com/kubescape/kubescape/v3/core/mocks"
v1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
"github.com/kubescape/opa-utils/reporthandling/apis"
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/stretchr/testify/assert"
"os"
"reflect"
"testing"
)
func TestExceedsSeverity(t *testing.T) {
@@ -187,23 +188,20 @@ type spyLogger struct {
setItems []spyLogMessage
}
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) 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) Fatal(msg string, details ...helpers.IDetails) {
firstDetail := details[0]

View File

@@ -35,7 +35,7 @@ func Test_validateControlScanInfo(t *testing.T) {
t.Run(
tc.Description,
func(t *testing.T) {
var want = tc.Want
var want error = tc.Want
got := validateControlScanInfo(tc.ScanInfo)
@@ -85,7 +85,7 @@ func Test_validateFrameworkScanInfo(t *testing.T) {
t.Run(
tc.Description,
func(t *testing.T) {
var want = tc.Want
var want error = tc.Want
got := validateFrameworkScanInfo(tc.ScanInfo)

View File

@@ -1,6 +1,7 @@
package scan
import (
"context"
"errors"
"fmt"
"strings"
@@ -10,6 +11,7 @@ import (
"github.com/kubescape/kubescape/v3/core/meta"
v1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
"github.com/kubescape/opa-utils/objectsenvelopes"
"github.com/spf13/cobra"
)
@@ -65,12 +67,13 @@ func getWorkloadCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comma
setWorkloadScanInfo(scanInfo, kind, name)
// todo: add api version if provided
results, err := ks.Scan(scanInfo)
ctx := context.TODO()
results, err := ks.Scan(ctx, scanInfo)
if err != nil {
logger.L().Fatal(err.Error())
}
if err = results.HandleResults(ks.Context(), scanInfo); err != nil {
if err = results.HandleResults(ctx); err != nil {
logger.L().Fatal(err.Error())
}
@@ -95,7 +98,7 @@ func setWorkloadScanInfo(scanInfo *cautils.ScanInfo, kind string, name string) {
scanInfo.ScanObject.SetKind(kind)
scanInfo.ScanObject.SetName(name)
scanInfo.SetPolicyIdentifiers([]string{"workloadscan", "allcontrols"}, v1.KindFramework)
scanInfo.SetPolicyIdentifiers([]string{"workloadscan"}, v1.KindFramework)
if scanInfo.FilePath != "" {
scanInfo.InputPatterns = []string{scanInfo.FilePath}

View File

@@ -28,10 +28,6 @@ func TestSetWorkloadScanInfo(t *testing.T) {
Identifier: "workloadscan",
Kind: v1.KindFramework,
},
{
Identifier: "allcontrols",
Kind: v1.KindFramework,
},
},
ScanType: cautils.ScanTypeWorkload,
ScanObject: &objectsenvelopes.ScanObject{
@@ -63,19 +59,12 @@ func TestSetWorkloadScanInfo(t *testing.T) {
t.Errorf("got: %v, want: %v", scanInfo.ScanObject.Metadata.Name, tc.want.ScanObject.Metadata.Name)
}
if len(scanInfo.PolicyIdentifier) != len(tc.want.PolicyIdentifier) {
t.Errorf("got: %v policy identifiers, want: %v", len(scanInfo.PolicyIdentifier), len(tc.want.PolicyIdentifier))
if len(scanInfo.PolicyIdentifier) != 1 {
t.Errorf("got: %v, want: %v", len(scanInfo.PolicyIdentifier), 1)
}
for i, wantPolicy := range tc.want.PolicyIdentifier {
if i < len(scanInfo.PolicyIdentifier) {
if scanInfo.PolicyIdentifier[i].Identifier != wantPolicy.Identifier {
t.Errorf("got: %v, want: %v", scanInfo.PolicyIdentifier[i].Identifier, wantPolicy.Identifier)
}
if scanInfo.PolicyIdentifier[i].Kind != wantPolicy.Kind {
t.Errorf("got: %v, want: %v", scanInfo.PolicyIdentifier[i].Kind, wantPolicy.Kind)
}
}
if scanInfo.PolicyIdentifier[0].Identifier != tc.want.PolicyIdentifier[0].Identifier {
t.Errorf("got: %v, want: %v", scanInfo.PolicyIdentifier[0].Identifier, tc.want.PolicyIdentifier[0].Identifier)
}
},
)
@@ -105,17 +94,3 @@ 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)
})
}

View File

@@ -50,7 +50,7 @@ func TestValidateImageScanInfo(t *testing.T) {
t.Run(
tc.Description,
func(t *testing.T) {
var want = tc.Want
var want error = tc.Want
got := ValidateImageScanInfo(tc.ScanInfo)

View File

@@ -5,7 +5,6 @@ import (
"os"
"reflect"
"testing"
"time"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v3/core/cautils"
@@ -21,23 +20,20 @@ type spyLogger struct {
setItems []spyLogMessage
}
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) 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) Fatal(msg string, details ...helpers.IDetails) {
firstDetail := details[0]

View File

@@ -5,11 +5,10 @@ package update
// kubescape update
import (
"context"
"fmt"
"strings"
"github.com/kubescape/kubescape/v3/core/meta"
"github.com/kubescape/backend/pkg/versioncheck"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
@@ -26,18 +25,17 @@ var updateCmdExamples = fmt.Sprintf(`
%[1]s update
`, cautils.ExecName())
func GetUpdateCmd(ks meta.IKubescape) *cobra.Command {
func GetUpdateCmd() *cobra.Command {
updateCmd := &cobra.Command{
Use: "update",
Short: "Update to latest release version",
Long: ``,
Example: updateCmdExamples,
RunE: func(_ *cobra.Command, args []string) error {
ctx := context.TODO()
v := versioncheck.NewVersionCheckHandler()
versionCheckRequest := versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "update", nil)
if err := v.CheckLatestVersion(ks.Context(), versionCheckRequest); err != nil {
return err
}
v.CheckLatestVersion(ctx, versionCheckRequest)
//Checking the user's version of kubescape to the latest release
if versioncheck.BuildNumber == "" || strings.Contains(versioncheck.BuildNumber, "rc") {

View File

@@ -1,18 +0,0 @@
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)
}

View File

@@ -9,10 +9,11 @@ import (
"github.com/kubescape/go-logger"
"github.com/kubescape/kubescape/v3/core/cautils"
"sigs.k8s.io/yaml"
"github.com/spf13/cobra"
admissionv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
var vapHelperCmdExamples = fmt.Sprintf(`
@@ -220,11 +221,9 @@ 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,
ParameterNotFoundAction: &paramAction,
Name: paramRefName,
}
}
// Marshal the policy binding to YAML

View File

@@ -1,37 +1,29 @@
package version
import (
"context"
"fmt"
"github.com/kubescape/kubescape/v3/core/meta"
"github.com/kubescape/backend/pkg/versioncheck"
"github.com/spf13/cobra"
)
func GetVersionCmd(ks meta.IKubescape, version, commit, date string) *cobra.Command {
func GetVersionCmd() *cobra.Command {
versionCmd := &cobra.Command{
Use: "version",
Short: "Get current version",
Long: ``,
RunE: func(cmd *cobra.Command, args []string) error {
v := versioncheck.NewIVersionCheckHandler(ks.Context())
_ = v.CheckLatestVersion(ks.Context(), versioncheck.NewVersionCheckRequest("", version, "", "", "version", nil))
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
ctx := context.TODO()
v := versioncheck.NewIVersionCheckHandler(ctx)
versionCheckRequest := versioncheck.NewVersionCheckRequest("", versioncheck.BuildNumber, "", "", "version", nil)
v.CheckLatestVersion(ctx, versionCheckRequest)
fmt.Fprintf(cmd.OutOrStdout(),
"Your current version is: %s\n",
version,
)
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"Build commit: %s\n",
commit,
)
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"Build date: %s\n",
date,
versionCheckRequest.ClientVersion,
)
return nil
},
}
return versionCmd
}
}

View File

@@ -2,12 +2,9 @@ package version
import (
"bytes"
"context"
"io"
"testing"
"github.com/kubescape/kubescape/v3/core/core"
"github.com/kubescape/backend/pkg/versioncheck"
"github.com/stretchr/testify/assert"
)
@@ -20,21 +17,20 @@ func TestGetVersionCmd(t *testing.T) {
}{
{
name: "Undefined Build Number",
buildNumber: "unknown",
want: "Your current version is: unknown\nBuild commit: \nBuild date: \n",
buildNumber: "",
want: "Your current version is: unknown\n",
},
{
name: "Defined Build Number: v3.0.1",
buildNumber: "v3.0.1",
want: "Your current version is: v3.0.1\nBuild commit: \nBuild date: \n",
want: "Your current version is: v3.0.1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
versioncheck.BuildNumber = tt.buildNumber
ks := core.NewKubescape(context.TODO())
if cmd := GetVersionCmd(ks, tt.buildNumber, "", ""); cmd != nil {
if cmd := GetVersionCmd(); cmd != nil {
buf := bytes.NewBufferString("")
cmd.SetOut(buf)
cmd.Execute()
@@ -46,4 +42,4 @@ func TestGetVersionCmd(t *testing.T) {
}
})
}
}
}

View File

@@ -1,248 +1,14 @@
# 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
# Kubescape core package
```go
package main
import (
"context"
"fmt"
"log"
// initialize kubescape
ks := core.NewKubescape()
"github.com/kubescape/kubescape/v3/core"
"github.com/kubescape/kubescape/v3/core/cautils"
)
// scan cluster
results, err := ks.Scan(&cautils.ScanInfo{})
func main() {
ctx := context.Background()
// convert scan results to json
jsonRes, err := results.ToJson()
// Initialize Kubescape
ks := core.NewKubescape(ctx)
// Configure scan
scanInfo := &cautils.ScanInfo{
// Scan the current cluster
ScanAll: true,
}
// Run scan
results, err := ks.Scan(scanInfo)
if err != nil {
log.Fatalf("Scan failed: %v", err)
}
// Convert results to JSON
jsonRes, err := results.ToJson()
if err != nil {
log.Fatalf("Failed to convert results: %v", err)
}
fmt.Println(string(jsonRes))
}
```
---
## API Reference
### Creating a Kubescape Instance
```go
// Create with context
ks := core.NewKubescape(ctx)
```
### Scanning
```go
// Scan with configuration
results, err := ks.Scan(scanInfo)
```
### Listing Frameworks and Controls
```go
// List available policies
err := ks.List(listPolicies)
```
### Downloading Artifacts
```go
// Download for offline use
err := ks.Download(downloadInfo)
```
### Image Scanning
```go
// Scan container image
exceedsSeverity, err := ks.ScanImage(imgScanInfo, scanInfo)
```
### Fixing Misconfigurations
```go
// Apply fixes to manifests
err := ks.Fix(fixInfo)
```
---
## Examples
### Scan a Specific Framework
```go
scanInfo := &cautils.ScanInfo{}
scanInfo.SetPolicyIdentifiers([]string{"nsa"}, "framework")
results, err := ks.Scan(scanInfo)
```
### Scan Specific Namespaces
```go
scanInfo := &cautils.ScanInfo{
IncludeNamespaces: "production,staging",
}
results, err := ks.Scan(scanInfo)
```
### Scan Local YAML Files
```go
scanInfo := &cautils.ScanInfo{
InputPatterns: []string{"/path/to/manifests"},
}
scanInfo.SetScanType(cautils.ScanTypeRepo)
results, err := ks.Scan(scanInfo)
```
### Export Results to Different Formats
```go
results, _ := ks.Scan(scanInfo)
// JSON
jsonData, _ := results.ToJson()
// Get summary
summary := results.GetData().Report.SummaryDetails
fmt.Printf("Compliance Score: %.2f%%\n", summary.ComplianceScore)
```
### Scan with Compliance Threshold
```go
scanInfo := &cautils.ScanInfo{
ComplianceThreshold: 80.0, // Fail if below 80%
}
results, err := ks.Scan(scanInfo)
if err != nil {
// Handle scan failure
}
// Check if threshold was exceeded
if results.GetData().Report.SummaryDetails.ComplianceScore < scanInfo.ComplianceThreshold {
log.Fatal("Compliance score below threshold")
}
```
---
## Configuration Options
### ScanInfo Fields
| Field | Type | Description |
|-------|------|-------------|
| `AccountID` | string | Kubescape SaaS account ID |
| `AccessKey` | string | Kubescape SaaS access key |
| `InputPatterns` | []string | Paths to scan (files, directories, URLs) |
| `ExcludedNamespaces` | string | Comma-separated namespaces to exclude |
| `IncludeNamespaces` | string | Comma-separated namespaces to include |
| `Format` | string | Output format (json, junit, sarif, etc.) |
| `Output` | string | Output file path |
| `VerboseMode` | bool | Show all resources in output |
| `FailThreshold` | float32 | Fail threshold percentage |
| `ComplianceThreshold` | float32 | Compliance threshold percentage |
| `UseExceptions` | string | Path to exceptions file |
| `UseArtifactsFrom` | string | Path to offline artifacts |
| `Submit` | bool | Submit results to SaaS |
| `Local` | bool | Keep results local (don't submit) |
---
## Error Handling
```go
results, err := ks.Scan(scanInfo)
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
log.Fatal("Scan timed out")
case errors.Is(err, context.Canceled):
log.Fatal("Scan was canceled")
default:
log.Fatalf("Scan error: %v", err)
}
}
```
---
## Thread Safety
The Kubescape instance is safe for concurrent use. You can run multiple scans in parallel:
```go
var wg sync.WaitGroup
for _, ns := range namespaces {
wg.Add(1)
go func(namespace string) {
defer wg.Done()
scanInfo := &cautils.ScanInfo{
IncludeNamespaces: namespace,
}
results, _ := ks.Scan(scanInfo)
// Process results...
}(ns)
}
wg.Wait()
```
---
## Related Documentation
- [CLI Reference](../docs/cli-reference.md)
- [Getting Started Guide](../docs/getting-started.md)
- [Architecture](../docs/architecture.md)
```

21
core/cautils/buildinfo.go Normal file
View File

@@ -0,0 +1,21 @@
package cautils
import (
"os"
"github.com/kubescape/backend/pkg/versioncheck"
)
var BuildNumber string
var Client string
func init() {
if BuildNumber != "" {
versioncheck.BuildNumber = BuildNumber
} else {
versioncheck.BuildNumber = os.Getenv("RELEASE")
}
if Client != "" {
versioncheck.Client = Client
}
}

View File

@@ -7,6 +7,8 @@ import (
"path/filepath"
"regexp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/google/uuid"
v1 "github.com/kubescape/backend/pkg/client/v1"
"github.com/kubescape/backend/pkg/servicediscovery"
@@ -17,14 +19,14 @@ import (
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/kubescape/v3/core/cautils/getter"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
configFileName string = "config"
kubescapeNamespace string = "kubescape"
kubescapeConfigMapName string = "kubescape-config" // deprecated - for backward compatibility
kubescapeConfigMapName string = "kubescape-config" // deprecated - for backward compatibility
kubescapeCloudConfigMapName string = "ks-cloud-config" // deprecated - for backward compatibility
cloudConfigMapLabelSelector string = "kubescape.io/infra=config"
credsLabelSelectors string = "kubescape.io/infra=credentials" //nolint:gosec
@@ -206,8 +208,6 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, accountID, accessKey, clu
loadConfigFromFile(c.configObj)
}
loadUrlsFromFile(c.configObj)
// second, load urls from config map
c.updateConfigEmptyFieldsFromKubescapeConfigMap()
@@ -271,12 +271,15 @@ 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 {
@@ -289,6 +292,30 @@ 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
}
@@ -371,7 +398,7 @@ func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
func loadConfigFromFile(configObj *ConfigObj) error {
dat, err := os.ReadFile(ConfigFileFullPath())
if err != nil {
return nil // no config file
return err
}
return readConfig(dat, configObj)
}
@@ -387,32 +414,6 @@ 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())
}
@@ -521,3 +522,9 @@ func GetTenantConfig(accountID, accessKey, clusterName, customClusterName string
}
// firstNonEmpty returns the first non-empty string
func firstNonEmpty(s1, s2 string) string {
if s1 != "" {
return s1
}
return s2
}

View File

@@ -4,10 +4,7 @@ import (
"context"
"sort"
"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/anchore/grype/grype/presenter/models"
"github.com/armosec/armoapi-go/armotypes"
"github.com/kubescape/k8s-interface/workloadinterface"
"github.com/kubescape/opa-utils/reporthandling"
@@ -23,14 +20,8 @@ type K8SResources map[string][]string
type ExternalResources map[string][]string
type ImageScanData struct {
Context pkg.Context
IgnoredMatches []match.IgnoredMatch
Image string
Matches match.Matches
Packages []pkg.Package
RemainingMatches *match.Matches
SBOM *sbom.SBOM
VulnerabilityProvider vulnerability.Provider
PresenterConfig *models.PresenterConfig
Image string
}
type ScanTypes string
@@ -68,43 +59,26 @@ type OPASessionObj struct {
SingleResourceScan workloadinterface.IWorkload // single resource scan
TopWorkloadsByScore []reporthandling.IResource
TemplateMapping map[string]MappingNodes // Map chart obj to template (only for rendering from path)
TriggeredByCLI bool
LabelsToCopy []string // Labels to copy from workloads to scan reports
}
func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework, k8sResources K8SResources, scanInfo *ScanInfo) *OPASessionObj {
clusterSize := estimateClusterSize(k8sResources)
if clusterSize < 100 {
clusterSize = 100
}
return &OPASessionObj{
Report: &reporthandlingv2.PostureReport{},
Policies: frameworks,
K8SResources: k8sResources,
AllResources: make(map[string]workloadinterface.IMetadata, clusterSize),
ResourcesResult: make(map[string]resourcesresults.Result, clusterSize),
ResourcesPrioritized: make(map[string]prioritization.PrioritizedResource, clusterSize/10),
InfoMap: make(map[string]apis.StatusInfo, clusterSize/10),
ResourceToControlsMap: make(map[string][]string, clusterSize/2),
ResourceSource: make(map[string]reporthandling.Source, clusterSize),
AllResources: make(map[string]workloadinterface.IMetadata),
ResourcesResult: make(map[string]resourcesresults.Result),
ResourcesPrioritized: make(map[string]prioritization.PrioritizedResource),
InfoMap: make(map[string]apis.StatusInfo),
ResourceToControlsMap: make(map[string][]string),
ResourceSource: make(map[string]reporthandling.Source),
SessionID: scanInfo.ScanID,
Metadata: scanInfoToScanMetadata(ctx, scanInfo),
OmitRawResources: scanInfo.OmitRawResources,
TriggeredByCLI: scanInfo.TriggeredByCLI,
TemplateMapping: make(map[string]MappingNodes, clusterSize/10),
LabelsToCopy: scanInfo.LabelsToCopy,
TemplateMapping: make(map[string]MappingNodes),
}
}
func estimateClusterSize(k8sResources K8SResources) int {
total := 0
for _, resourceIDs := range k8sResources {
total += len(resourceIDs)
}
return total
}
// SetTopWorkloads sets the top workloads by score
func (sessionObj *OPASessionObj) SetTopWorkloads() {
count := 0

View File

@@ -1,11 +1,12 @@
package cautils
import (
"golang.org/x/mod/semver"
"github.com/kubescape/backend/pkg/versioncheck"
"github.com/kubescape/opa-utils/reporthandling"
"github.com/kubescape/opa-utils/reporthandling/apis"
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
"golang.org/x/mod/semver"
)
func NewPolicies() *Policies {
@@ -76,18 +77,14 @@ func ShouldSkipRule(control reporthandling.Control, rule reporthandling.PolicyRu
// In local build (BuildNumber = ""):
// returns true only if rule doesn't have the "until" attribute
func isRuleKubescapeVersionCompatible(attributes map[string]interface{}, version string) bool {
normalizedVersion := version
if version != "" && !semver.IsValid(version) {
normalizedVersion = "v" + version
}
if from, ok := attributes["useFromKubescapeVersion"]; ok && from != nil {
switch sfrom := from.(type) {
case string:
if normalizedVersion != "" && semver.IsValid(normalizedVersion) && semver.Compare(normalizedVersion, sfrom) == -1 {
if version != "" && semver.Compare(version, sfrom) == -1 {
return false
}
default:
// Handle case where useFromKubescapeVersion is not a string
return false
}
}
@@ -95,10 +92,11 @@ func isRuleKubescapeVersionCompatible(attributes map[string]interface{}, version
if until, ok := attributes["useUntilKubescapeVersion"]; ok && until != nil {
switch suntil := until.(type) {
case string:
if normalizedVersion == "" || (semver.IsValid(normalizedVersion) && semver.Compare(normalizedVersion, suntil) >= 0) {
if version == "" || semver.Compare(version, suntil) >= 0 {
return false
}
default:
// Handle case where useUntilKubescapeVersion is not a string
return false
}
}

View File

@@ -7,14 +7,16 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/workloadinterface"
"golang.org/x/exp/slices"
"github.com/kubescape/go-logger"
"github.com/kubescape/opa-utils/objectsenvelopes"
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
"gopkg.in/yaml.v3"
)
@@ -322,7 +324,7 @@ func glob(root, pattern string, onlyDirectories bool) ([]string, error) {
return nil
}
fileFormat := getFileFormat(path)
if fileFormat != JSON_FILE_FORMAT && fileFormat != YAML_FILE_FORMAT {
if !(fileFormat == JSON_FILE_FORMAT || fileFormat == YAML_FILE_FORMAT) {
return nil
}
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {

View File

@@ -5,8 +5,10 @@ import (
"strings"
"github.com/armosec/armoapi-go/armotypes"
"github.com/kubescape/opa-utils/reporthandling"
"github.com/kubescape/opa-utils/reporthandling/attacktrack/v1alpha1"
"github.com/kubescape/regolibrary/v2/gitregostore"
)

View File

@@ -1,7 +1,7 @@
package getter
import (
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@@ -102,7 +102,7 @@ func TestHttpRespToString_NilResponse(t *testing.T) {
func TestHttpRespToString_ValidResponse(t *testing.T) {
resp := &http.Response{
Body: io.NopCloser(strings.NewReader("test response")),
Body: ioutil.NopCloser(strings.NewReader("test response")),
Status: "200 OK",
StatusCode: 200,
}
@@ -114,7 +114,7 @@ func TestHttpRespToString_ValidResponse(t *testing.T) {
// Returns an error with status and reason when unable to read response body.
func TestHttpRespToString_ReadError(t *testing.T) {
resp := &http.Response{
Body: io.NopCloser(strings.NewReader("test response")),
Body: ioutil.NopCloser(strings.NewReader("test response")),
}
resp.Body.Close()
result, err := httpRespToString(resp)
@@ -125,7 +125,7 @@ func TestHttpRespToString_ReadError(t *testing.T) {
// Returns an error with status and reason when unable to read response body.
func TestHttpRespToString_ErrorCodeLessThan200(t *testing.T) {
resp := &http.Response{
Body: io.NopCloser(strings.NewReader("test response")),
Body: ioutil.NopCloser(strings.NewReader("test response")),
StatusCode: 100,
}
resp.Body.Close()

View File

@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
@@ -24,6 +25,10 @@ const (
var (
globalMx sync.Mutex // a mutex to avoid data races on package globals while testing
testOptions = []v1.KSCloudOption{
v1.WithTrace(os.Getenv("DEBUG_TEST") != ""),
}
)
func TestGlobalKSCloudAPIConnector(t *testing.T) {
@@ -108,6 +113,8 @@ func mockAPIServer(t testing.TB) *testServer {
defer func() { _ = r.Body.Close() }()
_, _ = io.Copy(w, r.Body)
return
})
return server

View File

@@ -226,7 +226,7 @@ func (lp *LoadPolicy) GetControlsInputs(_ /* clusterName */ string) (map[string]
buf, err := os.ReadFile(filePath)
if err != nil {
formattedError := fmt.Errorf(
`error opening %s file, "controls-config" will be downloaded from ARMO management portal`,
`Error opening %s file, "controls-config" will be downloaded from ARMO management portal`,
fileName,
)
@@ -236,7 +236,7 @@ func (lp *LoadPolicy) GetControlsInputs(_ /* clusterName */ string) (map[string]
controlInputs := make(map[string][]string, 100) // from armotypes.Settings.PostureControlInputs
if err = json.Unmarshal(buf, &controlInputs); err != nil {
formattedError := fmt.Errorf(
`error reading %s file, %v, "controls-config" will be downloaded from ARMO management portal`,
`Error reading %s file, %v, "controls-config" will be downloaded from ARMO management portal`,
fileName, err,
)

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,6 @@
package cautils
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
@@ -12,15 +9,11 @@ import (
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/workloadinterface"
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
helmchart "helm.sh/helm/v3/pkg/chart"
helmloader "helm.sh/helm/v3/pkg/chart/loader"
helmchartutil "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
helmdownloader "helm.sh/helm/v3/pkg/downloader"
helmengine "helm.sh/helm/v3/pkg/engine"
helmgetter "helm.sh/helm/v3/pkg/getter"
helmregistry "helm.sh/helm/v3/pkg/registry"
"k8s.io/client-go/util/homedir"
)
type HelmChart struct {
@@ -32,51 +25,7 @@ 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
@@ -88,35 +37,6 @@ 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()
}

View File

@@ -8,8 +8,8 @@ import (
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/workloadinterface"
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
@@ -76,11 +76,7 @@ func getKustomizeDirectoryName(path string) string {
func (kd *KustomizeDirectory) GetWorkloads(kustomizeDirectoryPath string) (map[string][]workloadinterface.IMetadata, []error) {
fSys := filesys.MakeFsOnDisk()
// 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)
kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
resmap, err := kustomizer.Run(fSys, kustomizeDirectoryPath)
if err != nil {

View File

@@ -4,8 +4,6 @@ import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetKustomizeDirectoryName(t *testing.T) {
@@ -54,7 +52,7 @@ func TestGetKustomizeDirectoryName(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
tempFile := filepath.Join(tt.args.path, "kustomization.yaml")
if tt.createKustomization {
_ = os.WriteFile(tempFile, []byte(""), 0600)
_ = os.WriteFile(tempFile, []byte(""), 0644)
}
if got := getKustomizeDirectoryName(tt.args.path); got != tt.want {
t.Errorf("GetKustomizeDirectoryName() = %v, want %v", got, tt.want)
@@ -63,83 +61,3 @@ 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")
}

View File

@@ -81,7 +81,7 @@ func Test_GetRequestPayload(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.GetRequestPayload()
result := tc.OperatorScanInfo.GetRequestPayload()
assert.Equal(t, tc.result, result)
})
}
@@ -136,8 +136,8 @@ func Test_ValidatePayload(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := tc.GetRequestPayload()
result := tc.ValidatePayload(payload)
payload := tc.OperatorScanInfo.GetRequestPayload()
result := tc.OperatorScanInfo.ValidatePayload(payload)
assert.Equal(t, tc.result, result)
})
}

View File

@@ -170,6 +170,7 @@ func getInfoFromOne(output string, lastNumber int, isMapType bool) (value string
if isMapType {
lineNumber = lineNumber - 1
}
lastNumber = lineNumber
// save to structure
} else {
lineNumber = lastNumber

View File

@@ -78,7 +78,7 @@ func (p *portForward) StopPortForwarder() {
func (p *portForward) StartPortForwarder() error {
go func() {
p.ForwardPorts()
p.PortForwarder.ForwardPorts()
}()
p.waitForPortForwardReadiness()

View File

@@ -64,7 +64,7 @@ func Test_CreatePortForwarder(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewClientset(),
KubernetesClient: fake.NewSimpleClientset(),
K8SConfig: &rest.Config{
Host: "any",
},
@@ -105,7 +105,7 @@ func Test_GetPortForwardLocalhost(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewClientset(),
KubernetesClient: fake.NewSimpleClientset(),
K8SConfig: &rest.Config{
Host: "any",
},

View File

@@ -4,9 +4,10 @@ import (
"encoding/json"
"time"
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
"github.com/google/uuid"
"github.com/kubescape/k8s-interface/workloadinterface"
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
"github.com/kubescape/rbac-utils/rbacscanner"
"github.com/kubescape/rbac-utils/rbacutils"
)

View File

@@ -10,6 +10,8 @@ type RootInfo struct {
Logger string // logger level
LoggerName string // logger name ("pretty"/"zap"/"none")
CacheDir string // cached dir
DisableColor bool // Disable Color
EnableColor bool // Force enable Color
DiscoveryServerURL string // Discovery Server URL (See https://github.com/kubescape/backend/tree/main/pkg/servicediscovery)
KubeContext string // context name
}

View File

@@ -8,7 +8,6 @@ import (
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/kubescape/backend/pkg/versioncheck"
giturl "github.com/kubescape/go-git-url"
"github.com/kubescape/go-logger"
@@ -19,6 +18,8 @@ import (
"github.com/kubescape/opa-utils/objectsenvelopes"
"github.com/kubescape/opa-utils/reporthandling"
reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2"
"github.com/google/uuid"
)
type ScanningContext string
@@ -134,13 +135,10 @@ type ScanInfo struct {
EnableRegoPrint bool // true if print rego
ScanObject *objectsenvelopes.ScanObject // identifies a single resource (k8s object) to be scanned
IsDeletedScanObject bool // indicates whether the ScanObject is a deleted K8S resource
TriggeredByCLI bool // indicates whether the scan was triggered by the CLI
ScanType ScanTypes
ScanImages bool
UseDefaultMatchers bool
ChartPath string
FilePath string
LabelsToCopy []string // Labels to copy from workloads to scan reports
scanningContext *ScanningContext
cleanups []func()
}
@@ -322,9 +320,6 @@ 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 {
@@ -335,18 +330,6 @@ 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
@@ -472,8 +455,3 @@ 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://")
}

View File

@@ -88,16 +88,6 @@ 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) {

View File

@@ -1,28 +0,0 @@
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

View File

@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml

View File

@@ -1,13 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: nginx
newTag: "1.21"
replicas:
- name: test-app
count: 3

View File

@@ -1,6 +1,7 @@
package core
import (
"context"
"fmt"
"github.com/kubescape/kubescape/v3/core/cautils"
@@ -34,8 +35,8 @@ func (ks *Kubescape) ViewCachedConfig(viewConfig *metav1.ViewConfig) error {
return nil
}
func (ks *Kubescape) DeleteCachedConfig(deleteConfig *metav1.DeleteConfig) error {
func (ks *Kubescape) DeleteCachedConfig(ctx context.Context, deleteConfig *metav1.DeleteConfig) error {
tenant := cautils.GetTenantConfig("", "", "", "", nil) // change k8sinterface
return tenant.DeleteCachedConfig(ks.Context())
return tenant.DeleteCachedConfig(ctx)
}

View File

@@ -36,7 +36,7 @@ func getOperatorPod(k8sClient *k8sinterface.KubernetesApi, ns string) (*v1.Pod,
return nil, err
}
if len(pods.Items) != 1 {
return nil, errors.New("could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts")
return nil, errors.New("Could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts")
}
return &pods.Items[0], nil
@@ -90,8 +90,8 @@ func (a *OperatorAdapter) httpPostOperatorScanRequest(body apis.Commands) (strin
}
func (a *OperatorAdapter) OperatorScan() (string, error) {
payload := a.GetRequestPayload()
if err := a.ValidatePayload(payload); err != nil {
payload := a.OperatorScanInfo.GetRequestPayload()
if err := a.OperatorScanInfo.ValidatePayload(payload); err != nil {
return "", err
}
res, err := a.httpPostOperatorScanRequest(*payload)

View File

@@ -23,13 +23,13 @@ func Test_getOperatorPod(t *testing.T) {
name: "test error no operator exist",
createOperatorPod: false,
createAnotherOperatorPodWithSameLabel: false,
expectedError: fmt.Errorf("could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
expectedError: fmt.Errorf("Could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
},
{
name: "test error several operators exist",
createOperatorPod: true,
createAnotherOperatorPodWithSameLabel: true,
expectedError: fmt.Errorf("could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
expectedError: fmt.Errorf("Could not find the Kubescape Operator chart, please validate that the Kubescape Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
},
{
name: "test no error",
@@ -42,7 +42,7 @@ func Test_getOperatorPod(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewClientset(),
KubernetesClient: fake.NewSimpleClientset(),
Context: context.TODO(),
}

View File

@@ -44,12 +44,12 @@ func DownloadSupportCommands() []string {
return commands
}
func (ks *Kubescape) Download(downloadInfo *metav1.DownloadInfo) error {
func (ks *Kubescape) Download(ctx context.Context, downloadInfo *metav1.DownloadInfo) error {
setPathAndFilename(downloadInfo)
if err := os.MkdirAll(downloadInfo.Path, os.ModePerm); err != nil {
return err
}
if err := downloadArtifact(ks.Context(), downloadInfo, downloadFunc); err != nil {
if err := downloadArtifact(ctx, downloadInfo, downloadFunc); err != nil {
return err
}
return nil

View File

@@ -1,11 +1,13 @@
package core
import (
"context"
"fmt"
"strings"
"github.com/kubescape/go-logger"
metav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
"github.com/kubescape/kubescape/v3/core/pkg/fixhandler"
)
@@ -15,14 +17,14 @@ const (
confirmationQuestion = "Would you like to apply the changes to the files above? [y|n]: "
)
func (ks *Kubescape) Fix(fixInfo *metav1.FixInfo) error {
func (ks *Kubescape) Fix(ctx context.Context, fixInfo *metav1.FixInfo) error {
logger.L().Info("Reading report file...")
handler, err := fixhandler.NewFixHandler(fixInfo)
if err != nil {
return err
}
resourcesToFix := handler.PrepareResourcesToFix(ks.Context())
resourcesToFix := handler.PrepareResourcesToFix(ctx)
if len(resourcesToFix) == 0 {
logger.L().Info(noResourcesToFix)
@@ -41,14 +43,14 @@ func (ks *Kubescape) Fix(fixInfo *metav1.FixInfo) error {
return nil
}
updatedFilesCount, errors := handler.ApplyChanges(ks.Context(), resourcesToFix)
updatedFilesCount, errors := handler.ApplyChanges(ctx, resourcesToFix)
logger.L().Info(fmt.Sprintf("Fixed resources in %d files.", updatedFilesCount))
if len(errors) > 0 {
for _, err := range errors {
logger.L().Ctx(ks.Context()).Warning(err.Error())
logger.L().Ctx(ctx).Warning(err.Error())
}
return fmt.Errorf("failed to fix some resources, check the logs for more details")
return fmt.Errorf("Failed to fix some resources, check the logs for more details")
}
return nil
@@ -64,10 +66,9 @@ func userConfirmed() bool {
}
input = strings.ToLower(input)
switch input {
case "y", "yes":
if input == "y" || input == "yes" {
return true
case "n", "no":
} else if input == "n" || input == "no" {
return false
}
}

View File

@@ -33,11 +33,10 @@ 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 = originalStdin
os.Stdin = os.Stdin
}()
go func() {

View File

@@ -1,12 +1,10 @@
package core
import (
"encoding/json"
"context"
"fmt"
"os"
"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"
@@ -14,199 +12,39 @@ import (
"github.com/kubescape/kubescape/v3/pkg/imagescan"
)
// Data structure to represent attributes
type Attributes struct {
Registry string `json:"registry"`
Organization string `json:"organization,omitempty"`
ImageName string `json:"imageName"`
ImageTag string `json:"imageTag,omitempty"`
}
// Data structure for a target
type Target struct {
DesignatorType string `json:"designatorType"`
Attributes Attributes `json:"attributes"`
}
// Data structure for metadata
type Metadata struct {
Name string `json:"name"`
}
// Data structure for vulnerabilities and severities
type VulnerabilitiesIgnorePolicy struct {
Metadata Metadata `json:"metadata"`
Kind string `json:"kind"`
Targets []Target `json:"targets"`
Vulnerabilities []string `json:"vulnerabilities"`
Severities []string `json:"severities"`
}
// Loads excpetion policies from exceptions json object.
func GetImageExceptionsFromFile(filePath string) ([]VulnerabilitiesIgnorePolicy, error) {
// Read the JSON file
jsonFile, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading exceptions file: %w", err)
}
// Unmarshal the JSON data into an array of VulnerabilitiesIgnorePolicy
var policies []VulnerabilitiesIgnorePolicy
err = json.Unmarshal(jsonFile, &policies)
if err != nil {
return nil, fmt.Errorf("error unmarshaling exceptions file: %w", err)
}
return policies, nil
}
// This function will identify the registry, organization and image tag from the image name
func getAttributesFromImage(imgName string) (Attributes, error) {
canonicalImageName, err := cautils.NormalizeImageName(imgName)
if err != nil {
return Attributes{}, err
}
tokens := strings.Split(canonicalImageName, "/")
registry := tokens[0]
organization := tokens[1]
imageNameAndTag := strings.Split(tokens[2], ":")
imageName := imageNameAndTag[0]
// Intialize the image tag with default value
imageTag := "latest"
if len(imageNameAndTag) > 1 {
imageTag = imageNameAndTag[1]
}
attributes := Attributes{
Registry: registry,
Organization: organization,
ImageName: imageName,
ImageTag: imageTag,
}
return attributes, nil
}
// Checks if the target string matches the regex pattern
func regexStringMatch(pattern, target string) bool {
re, err := regexp.Compile(pattern)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to generate regular expression: %s", err))
return false
}
if re.MatchString(target) {
return true
}
return false
}
// Compares the registry, organization, image name, image tag against the targets specified
// in the exception policy object to check if the image being scanned qualifies for an
// exception policy.
func isTargetImage(targets []Target, attributes Attributes) bool {
for _, target := range targets {
if regexStringMatch(target.Attributes.Registry, attributes.Registry) && regexStringMatch(target.Attributes.Organization, attributes.Organization) && regexStringMatch(target.Attributes.ImageName, attributes.ImageName) && regexStringMatch(target.Attributes.ImageTag, attributes.ImageTag) {
return true
}
}
return false
}
// Generates a list of unique CVE-IDs and the severities which are to be excluded for
// the image being scanned.
func getUniqueVulnerabilitiesAndSeverities(policies []VulnerabilitiesIgnorePolicy, image string) ([]string, []string) {
// Create maps with slices as values to store unique vulnerabilities and severities (case-insensitive)
uniqueVulns := make(map[string][]string)
uniqueSevers := make(map[string][]string)
imageAttributes, err := getAttributesFromImage(image)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to generate image attributes: %s", err))
}
// Iterate over each policy and its vulnerabilities/severities
for _, policy := range policies {
// Include the exceptions only if the image is one of the targets
if isTargetImage(policy.Targets, imageAttributes) {
for _, vulnerability := range policy.Vulnerabilities {
// Add to slice directly
vulnerabilityUppercase := strings.ToUpper(vulnerability)
uniqueVulns[vulnerabilityUppercase] = append(uniqueVulns[vulnerabilityUppercase], vulnerability)
}
for _, severity := range policy.Severities {
// Add to slice directly
severityUppercase := strings.ToUpper(severity)
uniqueSevers[severityUppercase] = append(uniqueSevers[severityUppercase], severity)
}
}
}
// Extract unique keys (which are unique vulnerabilities/severities) and their slices
uniqueVulnsList := make([]string, 0, len(uniqueVulns))
for vuln := range uniqueVulns {
uniqueVulnsList = append(uniqueVulnsList, vuln)
}
uniqueSeversList := make([]string, 0, len(uniqueSevers))
for sever := range uniqueSevers {
uniqueSeversList = append(uniqueSeversList, sever)
}
return uniqueVulnsList, uniqueSeversList
}
func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (bool, error) {
func (ks *Kubescape) ScanImage(ctx context.Context, imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
logger.L().Start(fmt.Sprintf("Scanning image %s...", imgScanInfo.Image))
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()
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc := imagescan.NewScanService(dbCfg)
creds := imagescan.RegistryCredentials{
Username: imgScanInfo.Username,
Password: imgScanInfo.Password,
}
var vulnerabilityExceptions []string
var severityExceptions []string
if imgScanInfo.Exceptions != "" {
exceptionPolicies, err := GetImageExceptionsFromFile(imgScanInfo.Exceptions)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to load exceptions from file: %s", imgScanInfo.Exceptions))
return false, err
}
vulnerabilityExceptions, severityExceptions = getUniqueVulnerabilitiesAndSeverities(exceptionPolicies, imgScanInfo.Image)
}
imageScanData, err := svc.Scan(ks.Context(), imgScanInfo.Image, creds, vulnerabilityExceptions, severityExceptions)
scanResults, err := svc.Scan(ctx, imgScanInfo.Image, creds)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to scan image: %s", imgScanInfo.Image))
return false, err
return nil, err
}
logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", imgScanInfo.Image))
scanInfo.SetScanType(cautils.ScanTypeImage)
outputPrinters := GetOutputPrinters(scanInfo, ks.Context(), "")
outputPrinters := GetOutputPrinters(scanInfo, ctx, "")
uiPrinter := GetUIPrinter(ks.Context(), scanInfo, "")
uiPrinter := GetUIPrinter(ctx, scanInfo, "")
resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter)
resultsHandler.ImageScanData = []cautils.ImageScanData{*imageScanData}
resultsHandler.ImageScanData = []cautils.ImageScanData{
{
PresenterConfig: scanResults,
Image: imgScanInfo.Image,
},
}
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), imageScanData.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
return scanResults, resultsHandler.HandleResults(ctx)
}

View File

@@ -1,447 +0,0 @@
package core
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetImageExceptionsFromFile(t *testing.T) {
tests := []struct {
filePath string
expectedPolicies []VulnerabilitiesIgnorePolicy
expectedErr error
}{
{
filePath: "./testdata/exceptions.json",
expectedPolicies: []VulnerabilitiesIgnorePolicy{
{
Metadata: Metadata{
Name: "medium-severity-vulnerabilites-exceptions",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "docker.io",
Organization: "",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{},
Severities: []string{"medium"},
},
{
Metadata: Metadata{
Name: "exclude-allowed-hostPath-control",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "",
Organization: "",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
Severities: []string{"critical", "low"},
},
{
Metadata: Metadata{
Name: "regex-example",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "quay.*",
Organization: "kube*",
ImageName: "kubescape*",
ImageTag: "v2*",
},
},
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "docker.io",
Organization: ".*",
ImageName: "kube*",
ImageTag: "v3*",
},
},
},
Vulnerabilities: []string{"CVE-2023-6879", "CVE-2023-44487"},
Severities: []string{"critical", "low"},
},
},
expectedErr: nil,
},
{
filePath: "./testdata/empty_exceptions.json",
expectedPolicies: []VulnerabilitiesIgnorePolicy{},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.filePath, func(t *testing.T) {
policies, err := GetImageExceptionsFromFile(tt.filePath)
assert.Equal(t, tt.expectedPolicies, policies)
assert.Equal(t, tt.expectedErr, err)
})
}
}
func TestGetAttributesFromImage(t *testing.T) {
tests := []struct {
imageName string
expectedAttributes Attributes
expectedErr error
}{
{
imageName: "quay.io/kubescape/kubescape-cli:v3.0.0",
expectedAttributes: Attributes{
Registry: "quay.io",
Organization: "kubescape",
ImageName: "kubescape-cli",
ImageTag: "v3.0.0",
},
expectedErr: nil,
},
{
imageName: "alpine",
expectedAttributes: Attributes{
Registry: "docker.io",
Organization: "library",
ImageName: "alpine",
ImageTag: "latest",
},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.imageName, func(t *testing.T) {
attributes, err := getAttributesFromImage(tt.imageName)
assert.Equal(t, tt.expectedErr, err)
assert.Equal(t, tt.expectedAttributes, attributes)
})
}
}
func TestRegexStringMatch(t *testing.T) {
tests := []struct {
pattern string
target string
expected bool
}{
{
pattern: ".*",
target: "quay.io",
expected: true,
},
{
pattern: "kubescape",
target: "kubescape",
expected: true,
},
{
pattern: "kubescape*",
target: "kubescape-cli",
expected: true,
},
{
pattern: "",
target: "v3.0.0",
expected: true,
},
{
pattern: "docker.io",
target: "quay.io",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.target+"/"+tt.pattern, func(t *testing.T) {
assert.Equal(t, tt.expected, regexStringMatch(tt.pattern, tt.target))
})
}
}
func TestIsTargetImage(t *testing.T) {
tests := []struct {
targets []Target
attributes Attributes
expected bool
}{
{
targets: []Target{
{
Attributes: Attributes{
Registry: "docker.io",
Organization: ".*",
ImageName: ".*",
ImageTag: "",
},
},
},
attributes: Attributes{
Registry: "quay.io",
Organization: "kubescape",
ImageName: "kubescape-cli",
ImageTag: "v3.0.0",
},
expected: false,
},
{
targets: []Target{
{
Attributes: Attributes{
Registry: "quay.io",
Organization: "kubescape",
ImageName: "kubescape*",
ImageTag: "",
},
},
},
attributes: Attributes{
Registry: "quay.io",
Organization: "kubescape",
ImageName: "kubescape-cli",
ImageTag: "v3.0.0",
},
expected: true,
},
{
targets: []Target{
{
Attributes: Attributes{
Registry: "docker.io",
Organization: "library",
ImageName: "alpine",
ImageTag: "",
},
},
},
attributes: Attributes{
Registry: "docker.io",
Organization: "library",
ImageName: "alpine",
ImageTag: "latest",
},
expected: true,
},
{
targets: []Target{
{
Attributes: Attributes{
Registry: "quay.io",
Organization: "kubescape",
ImageName: "kubescape*",
ImageTag: "",
},
},
{
Attributes: Attributes{
Registry: "docker.io",
Organization: "library",
ImageName: "alpine",
ImageTag: "",
},
},
},
attributes: Attributes{
Registry: "docker.io",
Organization: "library",
ImageName: "alpine",
ImageTag: "latest",
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.attributes.Registry+"/"+tt.attributes.ImageName, func(t *testing.T) {
assert.Equal(t, tt.expected, isTargetImage(tt.targets, tt.attributes))
})
}
}
func TestGetVulnerabilitiesAndSeverities(t *testing.T) {
tests := []struct {
policies []VulnerabilitiesIgnorePolicy
image string
expectedVulnerabilities []string
expectedSeverities []string
}{
{
policies: []VulnerabilitiesIgnorePolicy{
{
Metadata: Metadata{
Name: "vulnerabilites-exceptions",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "",
Organization: "kubescape*",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{"CVE-2023-42365"},
Severities: []string{},
},
{
Metadata: Metadata{
Name: "exclude-allowed-hostPath-control",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "docker.io",
Organization: "",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
Severities: []string{"critical", "low"},
},
},
image: "quay.io/kubescape/kubescape-cli:v3.0.0",
expectedVulnerabilities: []string{"CVE-2023-42365"},
expectedSeverities: []string{},
},
{
policies: []VulnerabilitiesIgnorePolicy{
{
Metadata: Metadata{
Name: "medium-severity-vulnerabilites-exceptions",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "",
Organization: "",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{},
Severities: []string{"medium"},
},
{
Metadata: Metadata{
Name: "exclude-allowed-hostPath-control",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "quay.io",
Organization: "",
ImageName: "",
ImageTag: "",
},
},
},
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2023-42365"},
Severities: []string{},
},
},
image: "alpine",
expectedVulnerabilities: []string{},
expectedSeverities: []string{"MEDIUM"},
},
{
policies: []VulnerabilitiesIgnorePolicy{
{
Metadata: Metadata{
Name: "regex-example",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "quay.io",
Organization: "kube*",
ImageName: "kubescape*",
ImageTag: ".*",
},
},
},
Vulnerabilities: []string{},
Severities: []string{"critical"},
},
{
Metadata: Metadata{
Name: "only-for-docker-registry",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
Registry: "docker.io",
ImageTag: "v3*",
},
},
},
Vulnerabilities: []string{"CVE-2023-42366", "CVE-2022-28391"},
Severities: []string{"high"},
},
{
Metadata: Metadata{
Name: "exclude-allowed-hostPath-control",
},
Kind: "VulnerabilitiesIgnorePolicy",
Targets: []Target{
{
DesignatorType: "Attributes",
Attributes: Attributes{
ImageTag: "v3*",
},
},
},
Vulnerabilities: []string{"CVE-2022-30065", "CVE-2022-28391"},
Severities: []string{},
},
},
image: "quay.io/kubescape/kubescape-cli:v3.0.0",
expectedVulnerabilities: []string{"CVE-2022-30065", "CVE-2022-28391"},
expectedSeverities: []string{"CRITICAL"},
},
}
for _, tt := range tests {
t.Run(tt.image, func(t *testing.T) {
vulnerabilities, severities := getUniqueVulnerabilitiesAndSeverities(tt.policies, tt.image)
sort.Strings(tt.expectedVulnerabilities)
sort.Strings(vulnerabilities)
assert.Equal(t, tt.expectedVulnerabilities, vulnerabilities)
assert.Equal(t, tt.expectedSeverities, severities)
})
}
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"github.com/google/uuid"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/k8sinterface"
@@ -17,8 +16,11 @@ import (
printerv2 "github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer/v2"
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter"
reporterv2 "github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter/v2"
"github.com/kubescape/rbac-utils/rbacscanner"
"go.opentelemetry.io/otel"
"github.com/google/uuid"
"github.com/kubescape/rbac-utils/rbacscanner"
)
// getKubernetesApi
@@ -82,7 +84,7 @@ func getReporter(ctx context.Context, tenantConfig cautils.ITenantConfig, report
}
func getResourceHandler(ctx context.Context, scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, hostSensorHandler hostsensorutils.IHostSensor) resourcehandler.IResourceHandler {
_, span := otel.Tracer("").Start(ctx, "getResourceHandler")
ctx, span := otel.Tracer("").Start(ctx, "getResourceHandler")
defer span.End()
if len(scanInfo.InputPatterns) > 0 || k8s == nil {
@@ -90,11 +92,7 @@ func getResourceHandler(ctx context.Context, scanInfo *cautils.ScanInfo, tenantC
return resourcehandler.NewFileResourceHandler()
}
// 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()
}
getter.GetKSCloudAPIConnector()
rbacObjects := getRBACHandler(tenantConfig, k8s, scanInfo.Submit)
return resourcehandler.NewK8sResourceHandler(k8s, hostSensorHandler, rbacObjects, tenantConfig.GetContextName())
}
@@ -279,7 +277,7 @@ func getAttackTracksGetter(ctx context.Context, attackTracks, accountID string,
return downloadReleasedPolicy
}
// GetUIPrinter returns a printer that will be used to print to the programs UI (terminal)
// getUIPrinter returns a printer that will be used to print to the programs UI (terminal)
func GetUIPrinter(ctx context.Context, scanInfo *cautils.ScanInfo, clusterName string) printer.IPrinter {
var p printer.IPrinter
if helpers.ToLevel(logger.L().GetLevel()) >= helpers.WarningLevel {

View File

@@ -1,17 +1,7 @@
package core
import (
"context"
)
type Kubescape struct{}
type Kubescape struct {
Ctx context.Context
}
func (ks *Kubescape) Context() context.Context {
return ks.Ctx
}
func NewKubescape(ctx context.Context) *Kubescape {
return &Kubescape{Ctx: ctx}
func NewKubescape() *Kubescape {
return &Kubescape{}
}

View File

@@ -1,7 +1,6 @@
package core
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -9,8 +8,7 @@ import (
// The function should return a non-nil pointer.
func TestNewKubescape_ReturnsNonNilPointer(t *testing.T) {
ctx := context.TODO()
k := NewKubescape(ctx)
k := NewKubescape()
assert.NotNil(t, k)
}
@@ -21,6 +19,5 @@ func TestNewKubescape_DoesNotPanic(t *testing.T) {
t.Errorf("Function panicked: %v", r)
}
}()
ctx := context.TODO()
NewKubescape(ctx)
NewKubescape()
}

View File

@@ -7,13 +7,14 @@ import (
"sort"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/jwalton/gchalk"
"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){
@@ -37,18 +38,18 @@ func ListSupportActions() []string {
sort.Strings(commands)
return commands
}
func (ks *Kubescape) List(listPolicies *metav1.ListPolicies) error {
func (ks *Kubescape) List(ctx context.Context, listPolicies *metav1.ListPolicies) error {
if policyListerFunc, ok := listFunc[listPolicies.Target]; ok {
policies, err := policyListerFunc(ks.Context(), listPolicies)
policies, err := policyListerFunc(ctx, listPolicies)
if err != nil {
return err
}
policies = naturalSortPolicies(policies)
if listFormatFunction, ok := listFormatFunc[listPolicies.Format]; ok {
listFormatFunction(ks.Context(), listPolicies.Target, policies)
listFormatFunction(ctx, listPolicies.Target, policies)
} else {
return fmt.Errorf("invalid format \"%s\", supported formats: 'pretty-print'/'json' ", listPolicies.Format)
return fmt.Errorf("Invalid format \"%s\", Supported formats: 'pretty-print'/'json' ", listPolicies.Format)
}
return nil
@@ -99,19 +100,30 @@ func prettyPrintListFormat(ctx context.Context, targetPolicy string, policies []
return
}
policyTable := table.NewWriter()
policyTable.SetOutputMirror(printer.GetWriter(ctx, ""))
policyTable := tablewriter.NewWriter(printer.GetWriter(ctx, ""))
policyTable.SetAutoWrapText(true)
header := fmt.Sprintf("Supported %s", targetPolicy)
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
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.AppendRows(generatePolicyRows(policies))
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.Render()
}
@@ -122,32 +134,40 @@ func jsonListFormat(_ context.Context, _ string, policies []string) {
}
func prettyPrintControls(ctx context.Context, policies []string) {
controlsTable := table.NewWriter()
controlsTable.SetOutputMirror(printer.GetWriter(ctx, ""))
controlsTable := tablewriter.NewWriter(printer.GetWriter(ctx, ""))
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}})
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))
controlRows := generateControlRows(policies)
short := utils.CheckShortTerminalWidth(controlRows, table.Row{"Control ID", "Control name", "Docs", "Frameworks"})
short := utils.CheckShortTerminalWidth(controlRows, []string{"Control ID", "Control name", "Docs", "Frameworks"})
if short {
controlsTable.AppendHeader(table.Row{"Controls"})
controlsTable.SetAutoWrapText(false)
controlsTable.SetHeader([]string{"Controls"})
controlRows = shortFormatControlRows(controlRows)
} else {
controlsTable.AppendHeader(table.Row{"Control ID", "Control name", "Docs", "Frameworks"})
controlsTable.SetHeader([]string{"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...)
controlsTable.AppendRows(controlRows)
data := v2.Matrix{}
data = append(data, controlRows...)
controlsTable.AppendBulk(data)
controlsTable.Render()
}
func generateControlRows(policies []string) []table.Row {
rows := make([]table.Row, 0, len(policies))
func generateControlRows(policies []string) [][]string {
rows := [][]string{}
for _, control := range policies {
@@ -168,7 +188,7 @@ func generateControlRows(policies []string) []table.Row {
docs := cautils.GetControlLink(id)
currentRow := table.Row{id, control, docs, strings.ReplaceAll(framework, " ", "\n")}
currentRow := []string{id, control, docs, strings.Replace(framework, " ", "\n", -1)}
rows = append(rows, currentRow)
}
@@ -176,19 +196,20 @@ func generateControlRows(policies []string) []table.Row {
return rows
}
func generatePolicyRows(policies []string) []table.Row {
rows := make([]table.Row, 0, len(policies))
func generatePolicyRows(policies []string) [][]string {
rows := [][]string{}
for _, policy := range policies {
rows = append(rows, table.Row{policy})
currentRow := []string{policy}
rows = append(rows, currentRow)
}
return rows
}
func shortFormatControlRows(controlRows []table.Row) []table.Row {
rows := make([]table.Row, 0, len(controlRows))
func shortFormatControlRows(controlRows [][]string) [][]string {
rows := [][]string{}
for _, controlRow := range controlRows {
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.ReplaceAll(controlRow[3].(string), "\n", " "))})
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))})
}
return rows
}

View File

@@ -9,7 +9,6 @@ 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"
)
@@ -106,7 +105,7 @@ func TestGeneratePolicyRows_NonEmptyPolicyList(t *testing.T) {
result := generatePolicyRows(policies)
// Assert
assert.Equal(t, []table.Row{{"policy1"}, {"policy2"}, {"policy3"}}, result)
assert.Equal(t, [][]string{{"policy1"}, {"policy2"}, {"policy3"}}, result)
}
// Returns an empty 2D slice for an empty list of policies.
@@ -123,12 +122,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 := []table.Row{
controlRows := [][]string{
{"ID1", "Control 1", "Docs 1", "Framework 1"},
{"ID2", "Control 2", "Docs 2", "Framework 2"},
}
want := []table.Row{
want := [][]string{
{"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"},
}
@@ -140,12 +139,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 := []table.Row{
controlRows := [][]string{
{"ID1", "Control 1", "Docs 1", "Framework\n1"},
{"ID2", "Control 2", "Docs 2", "Framework\n2"},
}
want := []table.Row{
want := [][]string{
{"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"},
}
@@ -157,11 +156,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 := []table.Row{
controlRows := [][]string{
{"", "Control 1", "Docs 1", "Framework 1"},
}
want := []table.Row{
want := [][]string{
{"Control ID : \nControl Name : Control 1\nDocs : Docs 1\nFrameworks : Framework 1"},
}
@@ -172,11 +171,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 := []table.Row{
controlRows := [][]string{
{"ID1", "", "Docs 1", "Framework 1"},
}
want := []table.Row{
want := [][]string{
{"Control ID : ID1\nControl Name : \nDocs : Docs 1\nFrameworks : Framework 1"},
}
@@ -193,7 +192,7 @@ func TestGenerateControlRowsWithAllFields(t *testing.T) {
"3|Control 3|Framework 3",
}
want := []table.Row{
want := [][]string{
{"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"},
@@ -216,7 +215,7 @@ func TestGenerateControlRowsHandlesPoliciesWithEmptyStringOrNoPipesOrOnePipeMiss
"5|Control 5||Extra 5",
}
expectedRows := []table.Row{
expectedRows := [][]string{
{"", "", "https://hub.armosec.io/docs/", ""},
{"1", "", "https://hub.armosec.io/docs/1", ""},
{"2", "Control 2", "https://hub.armosec.io/docs/2", "Framework\n2"},
@@ -253,18 +252,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))
@@ -295,7 +294,7 @@ func TestGenerateTableWithMalformedPoliciesAndPrettyPrintHeadersAndRows(t *testi
os.Stdout = rescueStdout
want := `────────────┬──────────────┬───────────────────────────────┬────────────
want := `────────────┬──────────────┬───────────────────────────────┬────────────
│ Control ID │ Control name │ Docs │ Frameworks │
├────────────┼──────────────┼───────────────────────────────┼────────────┤
│ │ │ https://hub.armosec.io/docs/ │ │
@@ -303,18 +302,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))

View File

@@ -1,90 +1,64 @@
package core
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/anchore/clio"
grypejson "github.com/anchore/grype/grype/presenter/json"
"github.com/anchore/grype/grype/presenter"
"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"
ksmetav1 "github.com/kubescape/kubescape/v3/core/meta/datastructures/v1"
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling"
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/printer"
"github.com/kubescape/kubescape/v3/pkg/imagescan"
"github.com/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/kubescape/kubescape/v3/core/pkg/resultshandling"
log "github.com/sirupsen/logrus"
copaGrype "github.com/anubhav06/copa-grype/grype"
"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"
)
const (
copaProduct = "copa"
)
func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.ScanInfo) (bool, error) {
func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) {
// ===================== Scan the image =====================
logger.L().Start(fmt.Sprintf("Scanning image: %s", patchInfo.Image))
// Setup the scan service
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()
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc := imagescan.NewScanService(dbCfg)
creds := imagescan.RegistryCredentials{
Username: patchInfo.Username,
Password: patchInfo.Password,
}
// Scan the image
scanResults, err := svc.Scan(ks.Context(), patchInfo.Image, creds, nil, nil)
scanResults, err := svc.Scan(ctx, patchInfo.Image, creds)
if err != nil {
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)
return nil, err
}
// If the scan results ID is empty, set it to "grype"
if model.Descriptor.Name == "" {
model.Descriptor.Name = "grype"
if scanResults.ID.Name == "" {
scanResults.ID.Name = "grype"
}
// Save the scan results to a file in json format
pres := grypejson.NewPresenter(models.PresenterConfig{Document: model, SBOM: scanResults.SBOM})
pres := presenter.GetPresenter("json", "", false, *scanResults)
fileName := fmt.Sprintf("%s:%s.json", patchInfo.ImageName, patchInfo.ImageTag)
fileName = strings.ReplaceAll(fileName, "/", "-")
writer := printer.GetWriter(ks.Context(), fileName)
writer := printer.GetWriter(ctx, fileName)
if err = pres.Present(writer); err != nil {
return false, err
return nil, err
}
logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", patchInfo.Image))
@@ -97,8 +71,8 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
disableCopaLogger()
}
if err = copaPatch(ks.Context(), patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchedImageName, "", patchInfo.IgnoreError, patchInfo.BuildKitOpts); err != nil {
return false, err
if err = copaPatch(ctx, patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchedImageName, "", patchInfo.IgnoreError, patchInfo.BuildKitOpts); err != nil {
return nil, err
}
// Restore the output streams
@@ -110,9 +84,9 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
logger.L().Start(fmt.Sprintf("Re-scanning image: %s", patchedImageName))
scanResultsPatched, err := svc.Scan(ks.Context(), patchedImageName, creds, nil, nil)
scanResultsPatched, err := svc.Scan(ctx, patchedImageName, creds)
if err != nil {
return false, err
return nil, err
}
logger.L().StopSuccess(fmt.Sprintf("Successfully re-scanned image: %s", patchedImageName))
@@ -125,12 +99,17 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
// ===================== Results Handling =====================
scanInfo.SetScanType(cautils.ScanTypeImage)
outputPrinters := GetOutputPrinters(scanInfo, ks.Context(), "")
uiPrinter := GetUIPrinter(ks.Context(), scanInfo, "")
outputPrinters := GetOutputPrinters(scanInfo, ctx, "")
uiPrinter := GetUIPrinter(ctx, scanInfo, "")
resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter)
resultsHandler.ImageScanData = []cautils.ImageScanData{*scanResultsPatched}
resultsHandler.ImageScanData = []cautils.ImageScanData{
{
PresenterConfig: scanResultsPatched,
Image: patchedImageName,
},
}
return svc.ExceedsSeverityThreshold(imagescan.ParseSeverity(scanInfo.FailThresholdSeverity), scanResultsPatched.Matches), resultsHandler.HandleResults(ks.Context(), scanInfo)
return scanResultsPatched, resultsHandler.HandleResults(ctx)
}
func disableCopaLogger() {
@@ -184,183 +163,41 @@ 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
}
bkClient, err := buildkit.NewClient(ctx, bkOpts)
client, err := buildkit.NewClient(ctx, bkOpts)
if err != nil {
return fmt.Errorf("copa: error creating buildkit client :: %w", err)
return err
}
defer bkClient.Close()
defer client.Close()
dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
cfg := authprovider.DockerAuthProviderConfig{ConfigFile: dockerConfig}
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(cfg)}
solveOpt := client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterImage,
Attrs: map[string]string{
"name": patchedImageName,
"push": "true",
},
},
},
Frontend: "", // i.e. we are passing in the llb.Definition directly
Session: attachable, // used for authprovider, sshagentprovider and secretprovider
}
solveOpt.SourcePolicy, err = build.ReadSourcePolicy()
// Configure buildctl/client for use by package manager
config, err := buildkit.InitializeBuildkitConfig(ctx, client, image, updates)
if err != nil {
return fmt.Errorf("copa: error reading source policy :: %w", err)
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)
}
// Create package manager helper
var manager pkgmgr.PackageManager
if reportFile == "" {
// determine OS family
fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release")
if err != nil {
return nil, fmt.Errorf("unable to extract /etc/os-release file from state %w", err)
}
osType, err := getOSType(ctx, fileBytes)
if err != nil {
return nil, fmt.Errorf("copa: error getting os type :: %w", err)
}
osVersion, err := getOSVersion(ctx, fileBytes)
if err != nil {
return nil, fmt.Errorf("copa: error getting os version :: %w", err)
}
// get package manager based on os family type
manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder)
if err != nil {
return nil, fmt.Errorf("copa: error getting package manager for ostype=%s, version=%s :: %w", osType, osVersion, err)
}
// do not specify updates, will update all
updates = nil
} else {
// get package manager based on os family type
manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder)
if err != nil {
return nil, fmt.Errorf("copa: error getting package manager by family type: ostype=%s, osversion=%s :: %w", updates.Metadata.OS.Type, updates.Metadata.OS.Version, err)
}
}
// Export the patched image state to Docker
// TODO: Add support for other output modes as buildctl does.
log.Infof("Patching %d vulnerabilities", len(updates.Updates))
patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError)
log.Infof("Error is: %v", err)
if err != nil {
return nil, nil
}
platform := platforms.Normalize(platforms.DefaultSpec())
if platform.OS != "linux" {
platform.OS = "linux"
}
def, err := patchedImageState.Marshal(ctx, llb.Platform(platform))
if err != nil {
return nil, err
}
res, err := c.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
Evaluate: true,
})
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData)
// Currently can only validate updates if updating via scanner
if reportFile != "" {
// create a new manifest with the successfully patched packages
validatedManifest := &unversioned.UpdateManifest{
Metadata: unversioned.Metadata{
OS: unversioned.OS{
Type: updates.Metadata.OS.Type,
Version: updates.Metadata.OS.Version,
},
Config: unversioned.Config{
Arch: updates.Metadata.Config.Arch,
},
},
Updates: []unversioned.UpdatePackage{},
}
for _, update := range updates.Updates {
if !slices.Contains(errPkgs, update.Name) {
validatedManifest.Updates = append(validatedManifest.Updates, update)
}
}
}
return res, nil
}, buildChannel)
return err
}
func getOSType(ctx context.Context, osreleaseBytes []byte) (string, error) {
r := bytes.NewReader(osreleaseBytes)
osData, err := osrelease.Parse(ctx, r)
// Create package manager helper
pkgmgr, err := pkgmgr.GetPackageManager(updates.Metadata.OS.Type, config, workingFolder)
if err != nil {
return "", fmt.Errorf("unable to parse os-release data %w", err)
return 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)
// Export the patched image state to Docker
patchedImageState, _, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError)
if err != nil {
return "", fmt.Errorf("unable to parse os-release data %w", err)
return err
}
return osData["VERSION_ID"], nil
if err = buildkit.SolveToDocker(ctx, config.Client, patchedImageState, config.ConfigData, patchedImageName); err != nil {
return err
}
return nil
}
// This function adds support to copa for patching Kubescape produced results

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
mapset "github.com/deckarep/golang-set/v2"
"github.com/kubescape/backend/pkg/versioncheck"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
@@ -22,9 +21,11 @@ import (
"github.com/kubescape/kubescape/v3/core/pkg/resultshandling/reporter"
"github.com/kubescape/kubescape/v3/pkg/imagescan"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
"github.com/kubescape/opa-utils/resources"
"go.opentelemetry.io/otel"
"golang.org/x/exp/slices"
"k8s.io/client-go/kubernetes"
"github.com/kubescape/opa-utils/resources"
)
type componentInterfaces struct {
@@ -66,11 +67,9 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt
}
// ================== version testing ======================================
// 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))
}
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")
@@ -123,8 +122,8 @@ func GetOutputPrinters(scanInfo *cautils.ScanInfo, ctx context.Context, clusterN
return outputPrinters
}
func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
ctxInit, spanInit := otel.Tracer("").Start(ks.Context(), "initialization")
func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*resultshandling.ResultsHandler, error) {
ctxInit, spanInit := otel.Tracer("").Start(ctx, "initialization")
logger.L().Start("Kubescape scanner initializing...")
// ===================== Initialization =====================
@@ -134,31 +133,23 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
interfaces := getInterfaces(ctxInit, scanInfo)
interfaces.report.SetTenantConfig(interfaces.tenantConfig)
// 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
}
downloadReleasedPolicy := getter.NewDownloadReleasedPolicy() // download config inputs from github release
// set policy getter only after setting the customerGUID
scanInfo.PolicyGetter = getPolicyGetter(ctxInit, scanInfo.UseFrom, interfaces.tenantConfig.GetAccountID(), scanInfo.FrameworkScan, downloadReleasedPolicy)
scanInfo.ControlsInputsGetter = getConfigInputsGetter(ctxInit, scanInfo.ControlsInputs, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.ExceptionsGetter = getExceptionsGetter(ctxInit, scanInfo.UseExceptions, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.AttackTracksGetter = getAttackTracksGetter(ctxInit, scanInfo.AttackTracks, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.Getters.PolicyGetter = getPolicyGetter(ctxInit, scanInfo.UseFrom, interfaces.tenantConfig.GetAccountID(), scanInfo.FrameworkScan, downloadReleasedPolicy)
scanInfo.Getters.ControlsInputsGetter = getConfigInputsGetter(ctxInit, scanInfo.ControlsInputs, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.Getters.ExceptionsGetter = getExceptionsGetter(ctxInit, scanInfo.UseExceptions, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.Getters.AttackTracksGetter = getAttackTracksGetter(ctxInit, scanInfo.AttackTracks, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
// TODO - list supported frameworks/controls
if scanInfo.ScanAll {
scanInfo.SetPolicyIdentifiers(listFrameworksNames(scanInfo.PolicyGetter), apisv1.KindFramework)
scanInfo.SetPolicyIdentifiers(listFrameworksNames(scanInfo.Getters.PolicyGetter), apisv1.KindFramework)
}
// remove host scanner components
defer func() {
if err := interfaces.hostSensorHandler.TearDown(); err != nil {
logger.L().Ctx(ks.Context()).StopError("Failed to tear down host scanner", helpers.Error(err))
logger.L().Ctx(ctx).StopError("Failed to tear down host scanner", helpers.Error(err))
}
}()
@@ -187,7 +178,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
spanInit.End()
// ========================= opa testing =====================
ctxOpa, spanOpa := otel.Tracer("").Start(ks.Context(), "opa testing")
ctxOpa, spanOpa := otel.Tracer("").Start(ctx, "opa testing")
defer spanOpa.End()
deps := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig(), interfaces.tenantConfig.GetContextName())
@@ -200,8 +191,8 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
// ======================== prioritization ===================
if scanInfo.PrintAttackTree || isPrioritizationScanType(scanInfo.ScanType) {
_, spanPrioritization := otel.Tracer("").Start(ctxOpa, "prioritization")
if priotizationHandler, err := resourcesprioritization.NewResourcesPrioritizationHandler(ctxOpa, scanInfo.AttackTracksGetter, scanInfo.PrintAttackTree); err != nil {
logger.L().Ctx(ks.Context()).Warning("failed to get attack tracks, this may affect the scanning results", helpers.Error(err))
if priotizationHandler, err := resourcesprioritization.NewResourcesPrioritizationHandler(ctxOpa, scanInfo.Getters.AttackTracksGetter, scanInfo.PrintAttackTree); err != nil {
logger.L().Ctx(ctx).Warning("failed to get attack tracks, this may affect the scanning results", helpers.Error(err))
} else if err := priotizationHandler.PrioritizeResources(scanData); err != nil {
return resultsHandling, fmt.Errorf("%w", err)
}
@@ -212,7 +203,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
}
if scanInfo.ScanImages {
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling, scanInfo)
scanImages(scanInfo.ScanType, scanData, ctx, resultsHandling)
}
// ========================= results handling =====================
resultsHandling.SetData(scanData)
@@ -224,8 +215,8 @@ 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, scanInfo *cautils.ScanInfo) {
imagesToScan := mapset.NewSet[string]()
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler) {
var imagesToScan []string
if scanType == cautils.ScanTypeWorkload {
containers, err := workloadinterface.NewWorkloadObj(scanData.SingleResourceScan.GetObject()).GetContainers()
@@ -234,7 +225,9 @@ func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx
return
}
for _, container := range containers {
imagesToScan.Add(container.Image)
if !slices.Contains(imagesToScan, container.Image) {
imagesToScan = append(imagesToScan, container.Image)
}
}
} else {
for _, workload := range scanData.AllResources {
@@ -244,20 +237,17 @@ func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx
continue
}
for _, container := range containers {
imagesToScan.Add(container.Image)
if !slices.Contains(imagesToScan, container.Image) {
imagesToScan = append(imagesToScan, container.Image)
}
}
}
}
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()
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc := imagescan.NewScanService(dbCfg)
for img := range imagesToScan.Iter() {
for _, img := range imagesToScan {
logger.L().Start("Scanning", helpers.String("image", img))
if err := scanSingleImage(ctx, img, svc, resultsHandling); err != nil {
logger.L().StopError("failed to scan", helpers.String("image", img), helpers.Error(err))
@@ -266,27 +256,20 @@ 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)
scanResults, err := svc.Scan(ctx, img, imagescan.RegistryCredentials{})
if err != nil {
return err
}
resultsHandling.ImageScanData = append(resultsHandling.ImageScanData, *scanResults)
resultsHandling.ImageScanData = append(resultsHandling.ImageScanData, cautils.ImageScanData{
Image: img,
PresenterConfig: 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 != ""
}

Some files were not shown because too many files have changed in this diff Show More