diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000000..f647ab7ea1 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,102 @@ +name: build-image + +on: + workflow_dispatch: + inputs: + IMAGE_TAG: + required: true + type: string + description: "Image tag for the node-agent image" + STORAGE_REF: + required: false + type: string + default: "" + description: "Branch/tag/commit of k8sstormcenter/storage to use (leave empty to keep go.mod default)" + PLATFORMS: + type: boolean + required: false + default: false + description: "Build for both amd64 and arm64" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + packages: write + contents: read + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Update storage dependency + if: ${{ inputs.STORAGE_REF != '' }} + env: + STORAGE_REF: ${{ inputs.STORAGE_REF }} + GONOSUMCHECK: "*" + GOFLAGS: "" + run: | + echo "Replacing github.com/kubescape/storage with github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod edit -replace "github.com/kubescape/storage=github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod tidy + echo "Resolved storage version:" + grep "k8sstormcenter/storage" go.sum | head -1 + + - name: Ensure ig is installed + run: | + curl -L https://github.com/inspektor-gadget/inspektor-gadget/releases/download/v0.45.0/ig_0.45.0_amd64.deb -O + sudo dpkg -i ig_0.45.0_amd64.deb + + - name: Build gadgets + run: make gadgets + + - name: Set up QEMU + if: ${{ inputs.PLATFORMS }} + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: build/Dockerfile + tags: ghcr.io/${{ github.repository_owner }}/node-agent:${{ inputs.IMAGE_TAG }} + build-args: image_version=${{ inputs.IMAGE_TAG }} + platforms: ${{ inputs.PLATFORMS && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + push: true + + + trigger-component-tests: + needs: build + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Trigger component tests + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + STORAGE_TAG="${{ inputs.IMAGE_TAG }}" + NODE_AGENT_TAG="${{ inputs.IMAGE_TAG }}" + STORAGE_REF="${{ inputs.STORAGE_REF }}" + echo "Triggering component tests with STORAGE_TAG=${STORAGE_TAG} NODE_AGENT_TAG=${NODE_AGENT_TAG} STORAGE_REF=${STORAGE_REF}" + gh workflow run component-tests.yaml \ + --repo "${{ github.repository }}" \ + --ref "${{ github.ref_name }}" \ + -f STORAGE_TAG="${STORAGE_TAG}" \ + -f NODE_AGENT_TAG="${NODE_AGENT_TAG}" \ + -f STORAGE_REF="${STORAGE_REF}" diff --git a/.github/workflows/bypass.yaml b/.github/workflows/bypass.yaml index 49b38b42c5..be81f0260b 100644 --- a/.github/workflows/bypass.yaml +++ b/.github/workflows/bypass.yaml @@ -20,7 +20,7 @@ jobs: needs: reset-run-number uses: ./.github/workflows/incluster-comp-pr-merged.yaml with: - IMAGE_NAME: quay.io/${{ github.repository_owner }}/node-agent + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/node-agent IMAGE_TAG: v0.2.${{ needs.reset-run-number.outputs.run-number }} COMPONENT_NAME: nodeAgent CGO_ENABLED: 0 diff --git a/.github/workflows/component-tests.yaml b/.github/workflows/component-tests.yaml index 6c45625f05..2a59def39d 100644 --- a/.github/workflows/component-tests.yaml +++ b/.github/workflows/component-tests.yaml @@ -1,24 +1,150 @@ +# ============================================================================= +# Node Agent Component Tests +# ============================================================================= +# +# Architecture: +# There are TWO independent artifacts in play: +# +# 1. Node-agent container image — the eBPF runtime agent deployed INTO the +# Kind cluster via Helm. Lives in pkg/, cmd/, Makefile, Dockerfile, etc. +# Changes here require an image rebuild before tests can validate them. +# +# 2. Component test binary — a Go test suite compiled on-the-fly from +# tests/component_test.go via `go test`. Runs OUTSIDE the cluster on the +# CI runner. It drives the cluster by creating k8s resources, exec-ing +# into pods, and querying Alertmanager for alerts. +# Changes here do NOT require a node-agent image rebuild. +# +# Rebuild logic (on push): +# - If ONLY files under tests/ or .github/ changed → skip image build, +# run tests immediately against the existing 'latest' image. +# - If ANY agent code changed (pkg/, cmd/, go.mod, Makefile, …) → rebuild +# the node-agent image first, then run tests against the freshly built image. +# +# Manual trigger (workflow_dispatch): +# - Use the `build_image` checkbox to force an image rebuild. +# - Supply NODE_AGENT_TAG / STORAGE_TAG to pin specific pre-built images. +# ============================================================================= + name: Node Agent Component Tests on: - pull_request: - types: [synchronize, ready_for_review, opened, reopened] + push: + branches: + - feature/collapse-config-crd + workflow_dispatch: + inputs: + build_image: + description: 'Build and push a new container image for the test' + type: boolean + required: false + default: false + STORAGE_TAG: + description: 'Storage image tag (must match the tag built by storage/build)' + type: string + required: true + default: 'latest' + NODE_AGENT_TAG: + description: 'Node-agent image tag (must match the tag built by node-agent/build)' + type: string + required: true + default: 'latest' + STORAGE_REF: + description: 'Branch/tag/commit of k8sstormcenter/storage to use (leave empty to keep go.mod default)' + type: string + required: false + #default: 'a042ebaa0ec9280d69eac81b5eeaa4d0dfd1c558' # that was before rebaseing storage + default: 'e64d59a0e65e891b832f4f29bf770059ff0144b3' #in collapse crd concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + # ------------------------------------------------------------------- + # Detect what changed to decide whether an image rebuild is needed. + # On push: compare HEAD with HEAD~1. + # On workflow_dispatch: always outputs false (rebuild controlled by input). + # ------------------------------------------------------------------- + detect-changes: + runs-on: ubuntu-latest + outputs: + needs_rebuild: ${{ steps.check.outputs.needs_rebuild }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check for agent code changes + id: check + run: | + if [ "${{ github.event_name }}" != "push" ]; then + echo "Not a push event — rebuild decision deferred to workflow inputs" + echo "needs_rebuild=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + CHANGED=$(git diff --name-only HEAD~1 HEAD) + echo "=== Changed files ===" + echo "$CHANGED" + echo "" + + # Agent code = anything outside tests/ and .github/ + # These are the paths that end up in the node-agent container image. + AGENT_CHANGES=$(echo "$CHANGED" | grep -vE '^(tests/|\.github/)' || true) + + if [ -n "$AGENT_CHANGES" ]; then + echo "=== Agent code changed (rebuild needed) ===" + echo "$AGENT_CHANGES" + echo "needs_rebuild=true" >> "$GITHUB_OUTPUT" + else + echo "=== Only test/workflow files changed — no rebuild needed ===" + echo "needs_rebuild=false" >> "$GITHUB_OUTPUT" + fi + + # ------------------------------------------------------------------- + # Build and push the node-agent container image. + # Triggers when: + # - Manual dispatch with build_image=true, OR + # - Push event where agent code changed (detected above) + # ------------------------------------------------------------------- build-and-push-image: + needs: [detect-changes] + if: >- + (github.event_name == 'workflow_dispatch' && inputs.build_image == true) || + (needs.detect-changes.outputs.needs_rebuild == 'true') runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write steps: - name: Checkout code uses: actions/checkout@v4 - - name: Login to Quay.io + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - registry: quay.io/kubescape - username: ${{ secrets.QUAYIO_REGISTRY_USERNAME }} - password: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Update storage dependency + env: + STORAGE_REF: ${{ inputs.STORAGE_REF || 'e64d59a0e65e891b832f4f29bf770059ff0144b3' }} + GONOSUMCHECK: "*" + GOFLAGS: "" + run: | + echo "Replacing github.com/kubescape/storage with github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod edit -replace "github.com/kubescape/storage=github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod tidy + echo "Resolved storage version:" + grep "k8sstormcenter/storage" go.sum | head -1 + - name: Install IG run: | sudo apt-get update @@ -28,23 +154,42 @@ jobs: echo "Installing IG version: ${IG_VERSION}" curl -sL https://github.com/inspektor-gadget/inspektor-gadget/releases/download/${IG_VERSION}/ig-linux-${IG_ARCH}-${IG_VERSION}.tar.gz | sudo tar -C /usr/local/bin -xzf - ig sudo chmod +x /usr/local/bin/ig - - name: Build the Image and Push to Quay.io + - name: Build the Image and Push to GHCR id: build-and-push-image run: | COMMIT_HASH=$(git rev-parse --short HEAD) export IMAGE_TAG=test-${COMMIT_HASH} - export IMAGE_REPO=quay.io/kubescape/node-agent + export IMAGE_REPO=ghcr.io/${{ github.repository_owner }}/node-agent echo "image_repo=${IMAGE_REPO}" >> "$GITHUB_OUTPUT" - export IMAGE_NAME=quay.io/kubescape/node-agent:${IMAGE_TAG} + export IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/node-agent:${IMAGE_TAG} echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" make docker-build TAG=${IMAGE_TAG} IMAGE=${IMAGE_REPO} && make docker-push TAG=${IMAGE_TAG} IMAGE=${IMAGE_REPO} outputs: image_tag: ${{ steps.build-and-push-image.outputs.image_tag }} image_repo: ${{ steps.build-and-push-image.outputs.image_repo }} + # ------------------------------------------------------------------- + # Component tests. + # + # These are Go tests compiled from tests/component_test.go — they are + # NOT part of the node-agent container image. The test binary runs on + # the CI runner and talks to the Kind cluster via the k8s API. + # + # Dependency logic: + # - If build-and-push-image ran → waits for it, uses the freshly + # built image tag. + # - If build-and-push-image was skipped (tests-only change) → runs + # immediately with the default 'latest' image. + # - If build-and-push-image failed → tests do NOT run (no point + # testing against a stale image when code changed). + # ------------------------------------------------------------------- component-tests: + needs: [detect-changes, build-and-push-image] + # Run when build succeeded or was skipped; don't run if build failed. + if: >- + always() && !cancelled() && + (needs.build-and-push-image.result == 'success' || needs.build-and-push-image.result == 'skipped') runs-on: ubuntu-latest - needs: build-and-push-image continue-on-error: true strategy: matrix: @@ -71,7 +216,13 @@ jobs: Test_21_AlertOnPartialThenLearnNetworkTest, Test_22_AlertOnPartialNetworkProfileTest, Test_23_RuleCooldownTest, - Test_24_ProcessTreeDepthTest + Test_24_ProcessTreeDepthTest, + Test_27_ApplicationProfileOpens, + Test_28_UserDefinedNetworkNeighborhood, + Test_29_SignedApplicationProfile, + Test_30_TamperedSignedProfiles, + Test_31_TamperDetectionAlert, + Test_32_CollapseConfigurationCRD ] steps: - name: Checkout code @@ -97,17 +248,39 @@ jobs: helm upgrade --install prometheus prometheus-community/kube-prometheus-stack --set grafana.enabled=false --namespace monitoring --create-namespace --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false,prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false --set prometheus.prometheusSpec.maximumStartupDurationSeconds=300 --wait --timeout 5m # Check that the prometheus pod is running kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=prometheus -n monitoring --timeout=300s + + # Image selection logic: + # - If the build job ran and produced a tag → use it. + # - Otherwise fall back to the workflow_dispatch input or 'latest'. - name: Install Node Agent Chart run: | - STORAGE_TAG=$(./tests/scripts/storage-tag.sh) - echo "Storage tag that will be used: ${STORAGE_TAG}" - helm upgrade --install kubescape ./tests/chart --set clusterName=`kubectl config current-context` --set nodeAgent.image.tag=${{ needs.build-and-push-image.outputs.image_tag }} --set nodeAgent.image.repository=${{ needs.build-and-push-image.outputs.image_repo }} --set storage.image.tag=${STORAGE_TAG} -n kubescape --create-namespace --wait --timeout 5m --debug + STORAGE_TAG="${{ inputs.STORAGE_TAG || 'latest' }}" + echo "Storage tag: ${STORAGE_TAG}" + + # Prefer freshly built image; fall back to input or default. + IMAGE_TAG="${{ needs.build-and-push-image.outputs.image_tag || inputs.NODE_AGENT_TAG || 'latest' }}" + IMAGE_REPO="${{ needs.build-and-push-image.outputs.image_repo || 'ghcr.io/k8sstormcenter/node-agent' }}" + echo "Node Agent image: ${IMAGE_REPO}:${IMAGE_TAG}" + + # Log whether we're using a freshly built image or a pre-existing one. + if [ -n "${{ needs.build-and-push-image.outputs.image_tag }}" ]; then + echo ">>> Using FRESHLY BUILT image from this workflow run" + else + echo ">>> Using PRE-EXISTING image (no agent code changes detected)" + fi + + helm upgrade --install kubescape ./tests/chart --set clusterName=`kubectl config current-context` --set nodeAgent.image.tag=${IMAGE_TAG} --set nodeAgent.image.repository=${IMAGE_REPO} --set storage.image.tag=${STORAGE_TAG} -n kubescape --create-namespace --wait --timeout 5m --debug # Check that the node-agent pod is running kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=node-agent -n kubescape --timeout=300s sleep 5 - name: Run Port Forwarding run: | ./tests/scripts/port-forward.sh + + # The test binary is compiled from source here — it is NOT part of + # the node-agent container image. Only changes under tests/ affect + # it; agent code changes (pkg/, cmd/, …) require an image rebuild + # but do NOT change the test binary. - name: Set up Go env: CGO_ENABLED: 0 @@ -117,6 +290,17 @@ jobs: - name: Set unlimited memlock limit run: | sudo sh -c "ulimit -l unlimited" + - name: Update storage dependency + env: + STORAGE_REF: ${{ inputs.STORAGE_REF || 'e64d59a0e65e891b832f4f29bf770059ff0144b3' }} + GONOSUMCHECK: "*" + GOFLAGS: "" + run: | + echo "Replacing github.com/kubescape/storage with github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod edit -replace "github.com/kubescape/storage=github.com/k8sstormcenter/storage@${STORAGE_REF}" + go mod tidy + echo "Resolved storage version:" + grep "k8sstormcenter/storage" go.sum | head -1 - name: Run test run: | cd tests && go test -v ./... -run ${{ matrix.test }} --timeout=20m --tags=component @@ -128,3 +312,28 @@ jobs: echo "-----------------------------------------" echo "Storage logs" kubectl logs $(kubectl get pods -n kubescape -o name | grep storage) -n kubescape + + trigger-integration-tests: + needs: component-tests + if: >- + github.event_name == 'workflow_dispatch' && + inputs.STORAGE_TAG != '' && + inputs.NODE_AGENT_TAG != '' + runs-on: ubuntu-latest + steps: + - name: Trigger storage integration tests + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_PAT }} + run: | + STORAGE_TAG="${{ inputs.STORAGE_TAG }}" + NODE_AGENT_TAG="${{ inputs.NODE_AGENT_TAG }}" + echo "Triggering storage integration tests" + echo " node_agent_image=ghcr.io/${{ github.repository_owner }}/node-agent:${NODE_AGENT_TAG}" + echo " storage_image=ghcr.io/${{ github.repository_owner }}/storage:${STORAGE_TAG}" + gh workflow run manual-integration-tests.yml \ + --repo "${{ github.repository_owner }}/storage" \ + --ref "${{ github.ref_name }}" \ + -f branch="${{ github.ref_name }}" \ + -f branch_helm_chart=main \ + -f node_agent_image="ghcr.io/${{ github.repository_owner }}/node-agent:${NODE_AGENT_TAG}" \ + -f storage_image="ghcr.io/${{ github.repository_owner }}/storage:${STORAGE_TAG}" diff --git a/.github/workflows/incluster-comp-pr-merged.yaml b/.github/workflows/incluster-comp-pr-merged.yaml index dcf1338c44..587337a663 100644 --- a/.github/workflows/incluster-comp-pr-merged.yaml +++ b/.github/workflows/incluster-comp-pr-merged.yaml @@ -109,12 +109,12 @@ jobs: id: unit-test run: go test -exec sudo -v ./... - - name: Login to Quay + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - registry: quay.io - username: ${{ secrets.QUAYIO_REGISTRY_USERNAME }} - password: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 @@ -349,12 +349,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Quay + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - registry: quay.io - username: ${{ secrets.QUAYIO_REGISTRY_USERNAME }} - password: ${{ secrets.QUAYIO_REGISTRY_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Docker retag run: | diff --git a/.github/workflows/pr-created.yaml b/.github/workflows/pr-created.yaml index 856fa2bc26..76ad4f3cbf 100644 --- a/.github/workflows/pr-created.yaml +++ b/.github/workflows/pr-created.yaml @@ -13,6 +13,10 @@ concurrency: jobs: pr-created: + permissions: + pull-requests: write + security-events: write + contents: read uses: ./.github/workflows/incluster-comp-pr-created.yaml with: GO_VERSION: "1.25" diff --git a/.github/workflows/pr-merged.yaml b/.github/workflows/pr-merged.yaml index ddeee9206c..1e98658f59 100644 --- a/.github/workflows/pr-merged.yaml +++ b/.github/workflows/pr-merged.yaml @@ -35,7 +35,7 @@ jobs: pull-requests: read uses: ./.github/workflows/incluster-comp-pr-merged.yaml with: - IMAGE_NAME: quay.io/${{ github.repository_owner }}/node-agent + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/node-agent IMAGE_TAG: v0.3.${{ needs.reset-run-number.outputs.run-number }} COMPONENT_NAME: nodeAgent CGO_ENABLED: 0 diff --git a/.github/workflows/sign-object.yaml b/.github/workflows/sign-object.yaml new file mode 100644 index 0000000000..9233c62347 --- /dev/null +++ b/.github/workflows/sign-object.yaml @@ -0,0 +1,67 @@ +name: Build sign-object image + +on: + push: + branches: [main, feat/signature-verification] + paths: + - 'cmd/sign-object/**' + - 'pkg/signature/**' + - 'pkg/signature/profiles/**' + - 'go.mod' + - 'go.sum' + pull_request: + paths: + - 'cmd/sign-object/**' + - 'pkg/signature/**' + - 'pkg/signature/profiles/**' + workflow_dispatch: + inputs: + IMAGE_TAG: + required: false + type: string + default: 'latest' + description: 'Image tag for the sign-object image' + +permissions: + packages: write + contents: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/sign-object + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set image tag + id: tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.IMAGE_TAG }}" ]; then + echo "tag=${{ inputs.IMAGE_TAG }}" >> "$GITHUB_OUTPUT" + elif [ "${{ github.event_name }}" = "pull_request" ]; then + echo "tag=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v6 + with: + context: . + file: cmd/sign-object/Dockerfile + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + push: ${{ github.event_name != 'pull_request' }} diff --git a/.gitignore b/.gitignore index 135c3206a7..db15f79ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ temp resources/ebpf/falco/* node-agent __pycache__ -tracers.tar \ No newline at end of file +tracers.tar +vendor \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 7960ed3dd6..59d64bc034 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -96,7 +96,6 @@ func main() { clusterData.AccountID = credentials.Account logger.L().Info("credentials loaded", helpers.Int("accountLength", len(credentials.Account))) } - // to enable otel, set OTEL_COLLECTOR_SVC=otel-collector:4317 if otelHost, present := os.LookupEnv("OTEL_COLLECTOR_SVC"); present { ctx = logger.InitOtel("node-agent", @@ -226,7 +225,7 @@ func main() { ruleBindingCache = rulebindingcachev1.NewCache(cfg, k8sClient, ruleCreator) rulesWatcher := ruleswatcher.NewRulesWatcher(k8sClient, ruleCreator, func() { ruleBindingCache.RefreshRuleBindingsRules() - }) + }, &cfg) dWatcher.AddAdaptor(rulesWatcher) } @@ -294,10 +293,10 @@ func main() { ruleBindingNotify = make(chan rulebinding.RuleBindingNotify, 100) ruleBindingCache.AddNotifier(&ruleBindingNotify) - apc := applicationprofilecache.NewApplicationProfileCache(cfg, storageClient, k8sObjectCache) + apc := applicationprofilecache.NewApplicationProfileCache(cfg, storageClient, k8sObjectCache, exporter) apc.Start(ctx) - nnc := networkneighborhoodcache.NewNetworkNeighborhoodCache(cfg, storageClient, k8sObjectCache) + nnc := networkneighborhoodcache.NewNetworkNeighborhoodCache(cfg, storageClient, k8sObjectCache, exporter) nnc.Start(ctx) dc := dnscache.NewDnsCache(dnsResolver) diff --git a/cmd/sign-object/Dockerfile b/cmd/sign-object/Dockerfile new file mode 100644 index 0000000000..0f4284c473 --- /dev/null +++ b/cmd/sign-object/Dockerfile @@ -0,0 +1,20 @@ +FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS builder + +ENV GO111MODULE=on CGO_ENABLED=0 +WORKDIR /src +ARG TARGETOS TARGETARCH + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go mod download + +COPY . . +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /sign-object ./cmd/sign-object + +FROM gcr.io/distroless/static-debian13:latest +COPY --from=builder /sign-object /usr/local/bin/sign-object +WORKDIR /work +ENTRYPOINT ["sign-object"] diff --git a/cmd/sign-object/main.go b/cmd/sign-object/main.go new file mode 100644 index 0000000000..c803320b3c --- /dev/null +++ b/cmd/sign-object/main.go @@ -0,0 +1,550 @@ +package main + +import ( + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "flag" + "fmt" + "os" + "strings" + + k8syaml "k8s.io/apimachinery/pkg/util/yaml" + + rulemanagertypesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/node-agent/pkg/signature/profiles" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + sigsyaml "sigs.k8s.io/yaml" +) + +var ( + inputFile string + outputFile string + keyFile string + objectType string + useKeyless bool + verbose bool + strict bool + jsonOutput bool + publicOnly bool + command string +) + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + command = os.Args[1] + + argsRewritten := false + if command == "-h" || command == "--help" { + printUsage() + os.Exit(0) + } + if strings.HasPrefix(command, "-") { + command = "sign" + argsRewritten = true + } + + switch command { + case "sign", "": + parseSignFlags() + if argsRewritten { + os.Args = append([]string{"sign-object"}, os.Args[1:]...) + } + case "verify": + parseVerifyFlags() + os.Args = append([]string{"sign-object verify"}, os.Args[2:]...) + case "generate-keypair": + parseGenerateFlags() + os.Args = append([]string{"sign-object generate-keypair"}, os.Args[2:]...) + case "extract-signature": + parseExtractFlags() + os.Args = append([]string{"sign-object extract-signature"}, os.Args[2:]...) + case "help", "--help", "-h": + printUsage() + os.Exit(0) + default: + fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) + printUsage() + os.Exit(1) + } + + if err := runCommand(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func parseSignFlags() { + fs := flag.NewFlagSet("sign-object sign", flag.ExitOnError) + fs.StringVar(&inputFile, "file", "", "Input object YAML file (required)") + fs.StringVar(&outputFile, "output", "", "Output file for signed object (required)") + fs.StringVar(&keyFile, "key", "", "Path to private key file") + fs.StringVar(&objectType, "type", "auto", "Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto") + fs.BoolVar(&useKeyless, "keyless", false, "Use keyless signing (OIDC)") + fs.BoolVar(&verbose, "verbose", false, "Enable verbose logging") + + offset := 2 + if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-") { + offset = 1 + } + + if err := fs.Parse(os.Args[offset:]); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if inputFile == "" { + fmt.Fprintln(os.Stderr, "Error: --file is required") + fs.PrintDefaults() + os.Exit(1) + } + + if outputFile == "" { + fmt.Fprintln(os.Stderr, "Error: --output is required") + fs.PrintDefaults() + os.Exit(1) + } + + if !useKeyless && keyFile == "" { + fmt.Fprintln(os.Stderr, "Error: either --keyless or --key must be specified") + fs.PrintDefaults() + os.Exit(1) + } +} + +func parseVerifyFlags() { + fs := flag.NewFlagSet("sign-object verify", flag.ExitOnError) + fs.StringVar(&inputFile, "file", "", "Signed object YAML file (required)") + fs.StringVar(&objectType, "type", "auto", "Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto") + fs.BoolVar(&strict, "strict", true, "Require trusted issuer/identity") + fs.BoolVar(&verbose, "verbose", false, "Enable verbose logging") + + if err := fs.Parse(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if inputFile == "" { + fmt.Fprintln(os.Stderr, "Error: --file is required") + fs.PrintDefaults() + os.Exit(1) + } +} + +func parseGenerateFlags() { + fs := flag.NewFlagSet("sign-object generate-keypair", flag.ExitOnError) + fs.StringVar(&outputFile, "output", "", "Output PEM file") + fs.BoolVar(&publicOnly, "public-only", false, "Only output public key") + + if err := fs.Parse(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if outputFile == "" { + fmt.Fprintln(os.Stderr, "Error: --output is required") + fs.PrintDefaults() + os.Exit(1) + } +} + +func parseExtractFlags() { + fs := flag.NewFlagSet("sign-object extract-signature", flag.ExitOnError) + fs.StringVar(&inputFile, "file", "", "Signed object YAML file (required)") + fs.StringVar(&objectType, "type", "auto", "Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto") + fs.BoolVar(&jsonOutput, "json", false, "Output as JSON") + + if err := fs.Parse(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if inputFile == "" { + fmt.Fprintln(os.Stderr, "Error: --file is required") + fs.PrintDefaults() + os.Exit(1) + } +} + +func runCommand() error { + switch command { + case "sign", "": + return runSign() + case "verify": + return runVerify() + case "generate-keypair": + return runGenerateKeyPair() + case "extract-signature": + return runExtractSignature() + default: + return fmt.Errorf("unknown command: %s", command) + } +} + +func runSign() error { + data, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed to read input file: %w", err) + } + + if verbose { + fmt.Printf("Reading profile from: %s\n", inputFile) + fmt.Printf("Profile size: %d bytes\n", len(data)) + } + + profileAdapter, err := detectObjectType(objectType, data) + if err != nil { + return fmt.Errorf("failed to detect profile type: %w", err) + } + + if verbose { + fmt.Printf("Detected object type: %s\n", getObjectName(profileAdapter)) + } + + var signErr error + if useKeyless { + if verbose { + fmt.Println("Using keyless signing (OIDC)") + } + signErr = signature.SignObjectKeyless(profileAdapter) + } else { + if verbose { + fmt.Printf("Using local key from: %s\n", keyFile) + } + + keyData, err := os.ReadFile(keyFile) + if err != nil { + return fmt.Errorf("failed to read private key file: %w", err) + } + + block, _ := pem.Decode(keyData) + if block == nil { + return fmt.Errorf("failed to decode PEM block from key file") + } + + privateKey, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse EC private key: %w", err) + } + + signErr = signature.SignObject(profileAdapter, signature.WithPrivateKey(privateKey)) + } + + if signErr != nil { + return fmt.Errorf("failed to sign profile: %w", signErr) + } + + sig, err := signature.GetObjectSignature(profileAdapter) + if err != nil { + return fmt.Errorf("failed to get signature: %w", err) + } + + fmt.Printf("✓ Profile signed successfully\n") + fmt.Printf(" Issuer: %s\n", sig.Issuer) + fmt.Printf(" Identity: %s\n", sig.Identity) + fmt.Printf(" Timestamp: %d\n", sig.Timestamp) + + profileBytes, err := sigsyaml.Marshal(profileAdapter.GetUpdatedObject()) + if err != nil { + return fmt.Errorf("failed to marshal signed object: %w", err) + } + + if err := os.WriteFile(outputFile, profileBytes, 0644); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + + fmt.Printf("✓ Signed profile written to: %s\n", outputFile) + return nil +} + +func runVerify() error { + data, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + if verbose { + fmt.Printf("Reading profile from: %s\n", inputFile) + } + + profileAdapter, err := detectObjectType(objectType, data) + if err != nil { + return fmt.Errorf("failed to detect profile type: %w", err) + } + + sig, err := signature.GetObjectSignature(profileAdapter) + if err != nil { + return fmt.Errorf("profile is not signed: %w", err) + } + + fmt.Printf("Signature found:\n") + fmt.Printf(" Issuer: %s\n", sig.Issuer) + fmt.Printf(" Identity: %s\n", sig.Identity) + fmt.Printf(" Timestamp: %d\n", sig.Timestamp) + + var verifyErr error + if strict { + if verbose { + fmt.Println("Verifying with strict mode (keyless signatures must have issuer/identity)") + } + verifyErr = signature.VerifyObjectStrict(profileAdapter) + } else { + if verbose { + fmt.Println("Verifying in non-strict mode (allowing untrusted signatures)") + } + verifyErr = signature.VerifyObjectAllowUntrusted(profileAdapter) + } + + if verifyErr != nil { + return fmt.Errorf("signature verification failed: %w", verifyErr) + } + + fmt.Printf("✓ Signature verification successful\n") + return nil +} + +func runGenerateKeyPair() error { + adapter, err := signature.NewCosignAdapter(false) + if err != nil { + return fmt.Errorf("failed to create adapter: %w", err) + } + + pubKeyBytes, err := adapter.GetPublicKeyPEM() + if err != nil { + return fmt.Errorf("failed to get public key: %w", err) + } + + if publicOnly { + if err := os.WriteFile(outputFile, pubKeyBytes, 0644); err != nil { + return fmt.Errorf("failed to write public key file: %w", err) + } + + fmt.Printf("✓ Public key written to: %s\n", outputFile) + return nil + } + + privKeyBytes, err := adapter.GetPrivateKeyPEM() + if err != nil { + return fmt.Errorf("failed to get private key: %w", err) + } + + if err := os.WriteFile(outputFile, privKeyBytes, 0600); err != nil { + return fmt.Errorf("failed to write private key file: %w", err) + } + + pubKeyFile := outputFile + ".pub" + if err := os.WriteFile(pubKeyFile, pubKeyBytes, 0644); err != nil { + return fmt.Errorf("failed to write public key file: %w", err) + } + + fmt.Printf("✓ Private key written to: %s\n", outputFile) + fmt.Printf("✓ Public key written to: %s\n", pubKeyFile) + return nil +} + +func runExtractSignature() error { + data, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + profileAdapter, err := detectObjectType(objectType, data) + if err != nil { + return fmt.Errorf("failed to detect profile type: %w", err) + } + + sig, err := signature.GetObjectSignature(profileAdapter) + if err != nil { + return fmt.Errorf("profile is not signed: %w", err) + } + + sigInfo := map[string]interface{}{ + "signature_size": len(sig.Signature), + "certificate_size": len(sig.Certificate), + "issuer": sig.Issuer, + "identity": sig.Identity, + "timestamp": sig.Timestamp, + "signature_base64": base64.StdEncoding.EncodeToString(sig.Signature), + "certificate_base64": base64.StdEncoding.EncodeToString(sig.Certificate), + } + + if jsonOutput { + jsonData, err := json.MarshalIndent(sigInfo, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonData)) + } else { + fmt.Println("Signature Information:") + fmt.Printf(" Issuer: %s\n", sig.Issuer) + fmt.Printf(" Identity: %s\n", sig.Identity) + fmt.Printf(" Timestamp: %d\n", sig.Timestamp) + fmt.Printf(" Signature Size: %d bytes\n", len(sig.Signature)) + fmt.Printf(" Certificate Size: %d bytes\n", len(sig.Certificate)) + + block, _ := pem.Decode(sig.Certificate) + if block != nil { + fmt.Printf(" Certificate Type: %s\n", block.Type) + } + } + + return nil +} + +func detectObjectType(objectType string, data []byte) (signature.SignableObject, error) { + var decoded map[string]interface{} + if err := k8syaml.Unmarshal(data, &decoded); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) + } + + kind, _ := decoded["kind"].(string) + apiVersion, _ := decoded["apiVersion"].(string) + + if verbose { + fmt.Printf("Detected API: %s, Kind: %s\n", apiVersion, kind) + } + + if objectType != "auto" { + switch strings.ToLower(objectType) { + case "applicationprofile", "application-profile", "ap": + return loadApplicationProfile(data) + case "seccompprofile", "seccomp-profile", "sp": + return loadSeccompProfile(data) + case "networkneighborhood", "network-neighborhood", "nn": + return loadNetworkNeighborhood(data) + case "rules", "rule", "r": + return loadRules(data) + default: + return nil, fmt.Errorf("unknown object type: %s", objectType) + } + } + + if strings.Contains(strings.ToLower(apiVersion), "softwarecomposition") { + switch strings.ToLower(kind) { + case "applicationprofile", "application-profile": + return loadApplicationProfile(data) + case "seccompprofile", "seccomp-profile": + return loadSeccompProfile(data) + case "networkneighborhood", "network-neighborhood": + return loadNetworkNeighborhood(data) + } + } + + if strings.Contains(strings.ToLower(apiVersion), "kubescape.io") && strings.ToLower(kind) == "rules" { + return loadRules(data) + } + + return nil, fmt.Errorf("unable to auto-detect object type") +} + +func loadApplicationProfile(data []byte) (signature.SignableObject, error) { + var profile v1beta1.ApplicationProfile + if err := k8syaml.Unmarshal(data, &profile); err != nil { + return nil, fmt.Errorf("failed to unmarshal ApplicationProfile: %w", err) + } + return profiles.NewApplicationProfileAdapter(&profile), nil +} + +func loadSeccompProfile(data []byte) (signature.SignableObject, error) { + var profile v1beta1.SeccompProfile + if err := k8syaml.Unmarshal(data, &profile); err != nil { + return nil, fmt.Errorf("failed to unmarshal SeccompProfile: %w", err) + } + return profiles.NewSeccompProfileAdapter(&profile), nil +} + +func loadNetworkNeighborhood(data []byte) (signature.SignableObject, error) { + var nn v1beta1.NetworkNeighborhood + if err := k8syaml.Unmarshal(data, &nn); err != nil { + return nil, fmt.Errorf("failed to unmarshal NetworkNeighborhood: %w", err) + } + return profiles.NewNetworkNeighborhoodAdapter(&nn), nil +} + +func loadRules(data []byte) (signature.SignableObject, error) { + var rules rulemanagertypesv1.Rules + if err := k8syaml.Unmarshal(data, &rules); err != nil { + return nil, fmt.Errorf("failed to unmarshal Rules: %w", err) + } + return profiles.NewRulesAdapter(&rules), nil +} + +func getObjectName(profile signature.SignableObject) string { + if _, ok := profile.(*profiles.ApplicationProfileAdapter); ok { + return "ApplicationProfile" + } + if _, ok := profile.(*profiles.SeccompProfileAdapter); ok { + return "SeccompProfile" + } + if _, ok := profile.(*profiles.NetworkNeighborhoodAdapter); ok { + return "NetworkNeighborhood" + } + if _, ok := profile.(*profiles.RulesAdapter); ok { + return "Rules" + } + return "Unknown" +} + +func printUsage() { + fmt.Println(`sign-object - Sign and verify Kubernetes security objects + +USAGE: + sign-object [flags] + +COMMANDS: + sign Sign a profile (default command) + verify Verify a signed object + generate-keypair Generate a new ECDSA key pair + extract-signature Extract signature info from a profile + help Show this help message + +SIGN FLAGS: + --file Input object YAML file (required) + --output Output file for signed object (required) + --keyless Use keyless signing (OIDC) + --key Path to private key file + --type Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto (default: auto) + --verbose Enable verbose logging + +VERIFY FLAGS: + --file Signed object YAML file (required) + --type Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto (default: auto) + --strict Require trusted issuer/identity (default: true) + --verbose Enable verbose logging + +GENERATE-KEYPAIR FLAGS: + --output Output PEM file for private key (required) + --public-only Only output public key (no private key) + +EXTRACT-SIGNATURE FLAGS: + --file Signed object YAML file (required) + --type Object type: applicationprofile, seccompprofile, networkneighborhood, rules, or auto (default: auto) + --json Output as JSON + +EXAMPLES: + # Sign with keyless (OIDC) + sign-object --keyless --file object.yaml --output signed-object.yaml + + # Sign with local key + sign-object --key my-key.pem --file object.yaml --output signed-object.yaml + + # Verify a signed object + sign-object verify --file signed-object.yaml + + # Generate a key pair (writes my-key.pem and my-key.pem.pub) + sign-object generate-keypair --output my-key.pem + + # Generate only public key + sign-object generate-keypair --output my-key.pem --public-only + + # Extract signature information + sign-object extract-signature --file signed-object.yaml + +For more information, see: docs/signing/README.md`) +} diff --git a/go.mod b/go.mod index 14a8bd0385..4eda35231f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kubescape/node-agent -go 1.25.0 +go 1.25.5 require ( github.com/DmitriyVTitov/size v1.5.0 @@ -21,7 +21,8 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb - github.com/go-openapi/strfmt v0.23.0 + github.com/go-openapi/strfmt v0.26.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/cel-go v0.26.1 github.com/google/go-containerregistry v0.20.7 github.com/google/uuid v1.6.0 @@ -46,21 +47,25 @@ require ( github.com/prometheus/alertmanager v0.27.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/procfs v0.19.2 - github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af + github.com/sigstore/cosign/v3 v3.0.5 + github.com/sigstore/fulcio v1.8.5 + github.com/sigstore/rekor v1.5.1 + github.com/sigstore/sigstore v1.10.4 + github.com/sirupsen/logrus v1.9.4 github.com/spf13/afero v1.15.0 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/weaveworks/procspy v0.0.0-20150706124340-cb970aa190c3 go.uber.org/multierr v1.11.0 - golang.org/x/net v0.48.0 - golang.org/x/sys v0.40.0 + golang.org/x/net v0.50.0 + golang.org/x/sys v0.41.0 gonum.org/v1/plot v0.14.0 - google.golang.org/grpc v1.77.0 + google.golang.org/grpc v1.79.2 gopkg.in/mcuadros/go-syslog.v2 v2.3.0 istio.io/pkg v0.0.0-20231221211216-7635388a563e - k8s.io/api v0.35.0 - k8s.io/apimachinery v0.35.0 - k8s.io/client-go v0.35.0 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 k8s.io/cri-api v0.35.0 k8s.io/kubectl v0.34.1 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 @@ -70,14 +75,14 @@ require ( ) require ( - cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.3 // indirect - cloud.google.com/go/auth v0.16.2 // indirect + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.2 // indirect - cloud.google.com/go/monitoring v1.24.2 // indirect - cloud.google.com/go/storage v1.55.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.59.1 // indirect cyphar.com/go-pathrs v0.2.1 // indirect dario.cat/mergo v1.0.2 // indirect git.sr.ht/~sbinet/gg v0.5.0 // indirect @@ -89,8 +94,8 @@ require ( github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -125,7 +130,7 @@ require ( github.com/armosec/gojay v1.2.17 // indirect github.com/armosec/utils-go v0.0.58 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go v1.55.7 // indirect + github.com/aws/aws-sdk-go v1.55.8 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect @@ -154,6 +159,10 @@ require ( github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/briandowns/spinner v1.23.2 // indirect + github.com/buildkite/agent/v3 v3.115.4 // indirect + github.com/buildkite/go-pipeline v0.16.0 // indirect + github.com/buildkite/interpolate v0.1.5 // indirect + github.com/buildkite/roko v1.4.0 // indirect github.com/campoy/embedmd v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect @@ -164,7 +173,7 @@ require ( github.com/cilium/cilium v1.16.17 // indirect github.com/cloudflare/cbpfc v0.0.0-20240920015331-ff978e94500b // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/containerd v1.7.30 // indirect github.com/containerd/containerd/api v1.9.0 // indirect @@ -181,23 +190,26 @@ require ( github.com/containers/common v0.64.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/diskfs/go-diskfs v1.7.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.1.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/elliotchance/phpserialize v1.4.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/facebookincubator/nvdtools v0.1.5 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect @@ -206,10 +218,11 @@ require ( github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gammazero/deque v1.0.0 // indirect github.com/github/go-spdx/v2 v2.3.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-fonts/liberation v0.3.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -220,18 +233,29 @@ require ( github.com/go-ldap/ldap/v3 v3.4.10 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.2 // indirect - github.com/go-openapi/jsonpointer v0.21.2 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect github.com/go-pdf/fpdf v0.9.0 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/godbus/dbus/v5 v5.2.0 // indirect github.com/gofrs/flock v0.13.0 // indirect @@ -242,43 +266,47 @@ require ( github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gookit/color v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-getter v1.7.9 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect + github.com/in-toto/attestation v1.1.2 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jinzhu/copier v0.4.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/native v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/letsencrypt/boulder v0.20251110.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mackerelio/go-osstat v0.2.5 // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -291,7 +319,6 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect @@ -315,9 +342,11 @@ require ( github.com/notaryproject/notation-go v1.3.2 // indirect github.com/notaryproject/notation-plugin-framework-go v1.0.0 // indirect github.com/notaryproject/tspclient-go v1.0.0 // indirect + github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/nwaples/rardecode v1.1.3 // indirect github.com/nwaples/rardecode/v2 v2.1.1 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oleiade/reflections v1.1.0 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/tablewriter v1.0.9 // indirect @@ -325,23 +354,24 @@ require ( github.com/opcoder0/capabilities v0.0.0-20221222060822-17fd73bffd2a // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/selinux v1.13.1 // indirect - github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/packetcap/go-pcap v0.0.0-20250723190045-d00b185f30b7 // indirect github.com/pborman/indent v1.2.1 // indirect + github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.7.0 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect - github.com/puzpuzpuz/xsync/v2 v2.4.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect @@ -350,14 +380,17 @@ require ( github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sassoftware/go-rpmutils v0.4.0 // indirect + github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect github.com/seccomp/libseccomp-golang v0.11.0 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect - github.com/sigstore/sigstore v1.10.4 // indirect + github.com/sigstore/rekor-tiles/v2 v2.2.0 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.4 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sorairolake/lzip-go v0.3.7 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -373,7 +406,13 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/sif/v2 v2.22.0 // indirect github.com/sylabs/squashfs v1.0.6 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/therootcompany/xz v1.0.1 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 // indirect @@ -393,46 +432,45 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yl2chen/cidranger v1.0.2 // indirect github.com/zclconf/go-cty v1.16.3 // indirect - go.mongodb.org/mongo-driver v1.17.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.15.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel/log v0.16.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/sdk v1.41.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.242.0 // indirect - google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/api v0.269.0 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index b0b7e58046..8110b8d7a3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -41,8 +41,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo= -cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -104,8 +104,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= @@ -323,8 +323,8 @@ cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGE cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -344,6 +344,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= +cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= @@ -354,13 +356,13 @@ cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6 cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -384,8 +386,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= -cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -549,8 +551,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= -cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58= +cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -570,8 +572,8 @@ cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -627,6 +629,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= @@ -638,10 +642,25 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20250520111509-a70c2aa677fa h1:x6kFzdPgBoLbyoNkA/jny0ENpoEz4wqY8lPTQL2DPkg= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20250520111509-a70c2aa677fa/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= @@ -656,12 +675,12 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -698,6 +717,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anchore/archiver/v3 v3.5.3-0.20241210171143-5b1d8d1c7c51 h1:yhk+P8lF3ZiROjmaVRao9WGTRo4b/wYjoKEiAHWrKwc= @@ -774,8 +795,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= -github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= @@ -796,6 +817,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEd github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= @@ -841,6 +864,14 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1l github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buildkite/agent/v3 v3.115.4 h1:oxuLAjwHADBlTZuTrTb0JPt0FBcbGo55ZqDHPJ0jn+E= +github.com/buildkite/agent/v3 v3.115.4/go.mod h1:LKY99ujcnFwX8ihEXuMLuPIy3SPL2unKWGJ/DRLICr0= +github.com/buildkite/go-pipeline v0.16.0 h1:wEgWUMRAgSg1ZnWOoA3AovtYYdTvN0dLY1zwUWmPP+4= +github.com/buildkite/go-pipeline v0.16.0/go.mod h1:VE37qY3X5pmAKKUMoDZvPsHOQuyakB9cmXj9Qn6QasA= +github.com/buildkite/interpolate v0.1.5 h1:v2Ji3voik69UZlbfoqzx+qfcsOKLA61nHdU79VV+tPU= +github.com/buildkite/interpolate v0.1.5/go.mod h1:dHnrwHew5O8VNOAgMDpwRlFnhL5VSN6M1bHVmRZ9Ccc= +github.com/buildkite/roko v1.4.0 h1:DxixoCdpNqxu4/1lXrXbfsKbJSd7r1qoxtef/TT2J80= +github.com/buildkite/roko v1.4.0/go.mod h1:0vbODqUFEcVf4v2xVXRfZZRsqJVsCCHTG/TBRByGK4E= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -910,8 +941,8 @@ github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= @@ -956,8 +987,12 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/crewjam/rfc5424 v0.1.0 h1:MSeXJm22oKovLzWj44AHwaItjIMUMugYGkEzfa831H8= github.com/crewjam/rfc5424 v0.1.0/go.mod h1:RCi9M3xHVOeerf6ULZzqv2xOGRO/zYaVUeRyPnBW3gQ= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -969,6 +1004,11 @@ github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb/go.mod h1:B3tI9iGHi4i github.com/dghubble/trie v0.1.0 h1:kJnjBLFFElBwS60N4tkPvnLhnpcDxbBjIulgI8CpNGM= github.com/dghubble/trie v0.1.0/go.mod h1:sOmnzfBNH7H92ow2292dDFWNsVQuh/izuD7otCYb1ak= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8= github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k= github.com/distribution/distribution v2.8.2+incompatible h1:k9+4DKdOG+quPFZXT/mUsiQrGu9vYCp+dXpuPkuqhk8= @@ -983,8 +1023,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk= @@ -1006,8 +1046,8 @@ github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3 github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -1022,10 +1062,10 @@ github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPO github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -1033,8 +1073,8 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= @@ -1066,13 +1106,15 @@ github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34= github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -1085,6 +1127,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -1129,26 +1173,54 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= -github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= -github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= +github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= @@ -1157,14 +1229,20 @@ github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5 github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -1184,6 +1262,8 @@ github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp4 github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -1235,6 +1315,8 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= @@ -1260,7 +1342,11 @@ github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4p github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= @@ -1283,6 +1369,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -1293,6 +1380,9 @@ github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxV github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= +github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -1302,8 +1392,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -1317,8 +1407,8 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= @@ -1342,12 +1432,14 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1361,6 +1453,8 @@ github.com/hashicorp/go-getter v1.7.9 h1:G9gcjrDixz7glqJ+ll5IWvggSBR+R0B54DSRt4q github.com/hashicorp/go-getter v1.7.9/go.mod h1:dyFCmT1AQkDfOIt9NH8pw9XBDqNrIKJT5ylbpi7zPNE= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -1369,10 +1463,19 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -1388,6 +1491,8 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -1397,6 +1502,11 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -1408,11 +1518,21 @@ github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/iceber/iouring-go v0.0.0-20230403020409-002cfd2e2a90 h1:xrtfZokN++5kencK33hn2Kx3Uj8tGnjMEhdt6FMvHD0= github.com/iceber/iouring-go v0.0.0-20230403020409-002cfd2e2a90/go.mod h1:LEzdaZarZ5aqROlLIwJ4P7h3+4o71008fSy6wpaEB+s= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= @@ -1427,16 +1547,22 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/joncrlsn/dque v0.0.0-20241024143830-7723fd131a64 h1:fmH2K7R8pZJ0wVvJyGFmDnECuAE3NLjfAoJkN9mtfc8= github.com/joncrlsn/dque v0.0.0-20241024143830-7723fd131a64/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -1477,8 +1603,8 @@ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -1503,13 +1629,13 @@ github.com/kubescape/go-logger v0.0.24 h1:JRNlblY16Ty7hD6MSYNPvWYDxNzVAufsDDX/sZ github.com/kubescape/go-logger v0.0.24/go.mod h1:sMPVCr3VpW/e+SeMaXig5kClGvmZbDXN8YktUeNU4nY= github.com/kubescape/k8s-interface v0.0.204 h1:YkphM8aozocUazKpp0H37By/KZjUjnKeoYqP1b7uBWk= github.com/kubescape/k8s-interface v0.0.204/go.mod h1:d4NVhL81bVXe8yEXlkT4ZHrt3iEppEIN39b8N1oXm5s= -github.com/kubescape/storage v0.0.247 h1:Xf0ScExy7oT/NrZz9732tX/9V3/xudtIeHWKlNxXdxc= -github.com/kubescape/storage v0.0.247/go.mod h1:huYJIFh7TUAlV0W3+cmOh7KoJnWRcbWtGw0kY9YIrjU= github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf h1:hI0jVwrB6fT4GJWvuUjzObfci1CUknrZdRHfnRVtKM0= github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf/go.mod h1:Il5baM40PV9cTt4OGdLMeTRRAai3TMfvImu31itIeCM= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/letsencrypt/boulder v0.20251110.0 h1:J8MnKICeilO91dyQ2n5eBbab24neHzUpYMUIOdOtbjc= +github.com/letsencrypt/boulder v0.20251110.0/go.mod h1:ogKCJQwll82m7OVHWyTuf8eeFCjuzdRQlgnZcCl0V+8= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= @@ -1528,8 +1654,6 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/matthyx/inspektor-gadget v0.0.0-20260226175242-c524fbad47d9 h1:5SElOPiaA2SKDGnLiWqocww+YagkLL9FPBBeMzKNTIg= github.com/matthyx/inspektor-gadget v0.0.0-20260226175242-c524fbad47d9/go.mod h1:V4TgEmWo37K72pQvC7XuRQssysrxIIkrNX4TtEkgiE0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -1599,8 +1723,8 @@ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTS github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -1650,6 +1774,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncw/directio v1.0.5 h1:JSUBhdjEvVaJvOoyPAbcW0fnd0tvRXD76wEfZ1KcQz4= @@ -1667,16 +1793,24 @@ github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+ github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics= github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4= github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew= github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw= github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg= +github.com/oleiade/reflections v1.1.0 h1:D+I/UsXQB4esMathlt0kkZRJZdUDmhv5zGi/HOwYTWo= +github.com/oleiade/reflections v1.1.0/go.mod h1:mCxx0QseeVCHs5Um5HhJeCKVC7AwS8kO67tky4rdisA= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= @@ -1685,9 +1819,18 @@ github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTH github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/olvrng/ujson v1.1.0 h1:8xVUzVlqwdMVWh5d1UHBtLQ1D50nxoPuPEq9Wozs8oA= github.com/olvrng/ujson v1.1.0/go.mod h1:Mz4G3RODTUfbkKyvi0lgmPx/7vd3Saksk+1jgk8s9xo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opcoder0/capabilities v0.0.0-20221222060822-17fd73bffd2a h1:sbMMqulR2c6d2aeqOg5kzWv87unK0O4V78Dl1+YG4ys= @@ -1704,8 +1847,6 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/packetcap/go-pcap v0.0.0-20250723190045-d00b185f30b7 h1:MfXxQU9tEe3zmyLVVwE8gJwQVtsG2aqzBkFNz0N6eAo= @@ -1714,8 +1855,11 @@ github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZ github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -1736,6 +1880,8 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1772,16 +1918,16 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/puzpuzpuz/xsync/v2 v2.4.1 h1:aGdE1C/HaR/QC6YAFdtZXi60Df8/qBIrs8PKrzkItcM= -github.com/puzpuzpuz/xsync/v2 v2.4.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -1802,6 +1948,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20240229202106-bca3154da60a h1:4VFls9SuqkqeioVevnaeTXrYKQ7JiEsxqKHfxp+/ovA= github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20240229202106-bca3154da60a/go.mod h1:2zG1g57bc+D6FpNc68gsRXJgkidteqTMhWiiUP3m8UE= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= @@ -1817,6 +1965,10 @@ github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -1824,8 +1976,8 @@ github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5 github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/seccomp/libseccomp-golang v0.11.0 h1:SDkcBRqGLP+sezmMACkxO1EfgbghxIxnRKfd6mHUEis= github.com/seccomp/libseccomp-golang v0.11.0/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= -github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= -github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= +github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= +github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -1857,15 +2009,35 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sigstore/cosign/v3 v3.0.5 h1:c1zPqjU+H4wmirgysC+AkWMg7a7fykyOYF/m+F1150I= +github.com/sigstore/cosign/v3 v3.0.5/go.mod h1:ble1vMvJagCFyTIDkibCq6MIHiWDw00JNYl0f9rB4T4= +github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= +github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.5.1 h1:Ca1egHRWRuDvXV4tZu9aXEXc3Gej9FG+HKeapV9OAMQ= +github.com/sigstore/rekor v1.5.1/go.mod h1:gTLDuZuo3SyQCuZvKqwRPA79Qo/2rw39/WtLP/rZjUQ= +github.com/sigstore/rekor-tiles/v2 v2.2.0 h1:QwJNwxT+k5A3id+Hrg+8vYcNsTaB0Sj51xjfW2rKyAs= +github.com/sigstore/rekor-tiles/v2 v2.2.0/go.mod h1:/WNRYctHKdxcjgXydYwO5OclW72Zqh6fNHSyGE8zQOE= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4/go.mod h1:Rstj47WpJym25il8j4jTL0BfikzP/9AhVD+DsBcYzZc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4 h1:G7yOv8bxk3zIEEZyVCixPxtePIAm+t3ZWSaKRPzVw+o= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4/go.mod h1:hxJelB/bRItMYOzi6qD9xEKjse2QZcikh4TbysfdDHc= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4 h1:Qxt6dE4IwhJ6gIXmg2q4S/SeqEDSZ29nmfsv7Zb6LL4= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4/go.mod h1:hJVeNOwarqfyALjOwsf0OR8YA/A96NABucEaQumPr30= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4 h1:KVavYMPfSf5NryOl6VrZ9nRG3fXOOJOPp7Czk/YCPkM= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4/go.mod h1:J7CA1AaBkyK8dYq6EdQANhj+8oEcsA7PrIp088qgPiY= +github.com/sigstore/timestamp-authority/v2 v2.0.4 h1:65IBa4LUeFWDQu9hiTt5lBpi/F5jonJWZtH6VLn4InU= +github.com/sigstore/timestamp-authority/v2 v2.0.4/go.mod h1:EXJLiMDBqRPlzC02hPiFSiYTCqSuUpU68a4vr0DFePM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= -github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/sorairolake/lzip-go v0.3.7 h1:vP2uiD/NoklLyzYMdgOWkZME0ulkSfVTTE4MNRKCwNs= @@ -1916,6 +2088,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1932,11 +2105,31 @@ github.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA= github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs= github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= +github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -1988,6 +2181,16 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1995,6 +2198,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -2004,8 +2209,6 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= -go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -2018,47 +2221,49 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 h1:/+/+UjlXjFcdDlXxKL1PouzX8Z2Vl0OxolRKeBEgYDw= go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0/go.mod h1:Ldm/PDuzY2DP7IypudopCR3OCOW42NJlN9+mNEroevo= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= -go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= +go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -2066,8 +2271,8 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -2100,8 +2305,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2117,8 +2322,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2167,8 +2372,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2201,6 +2406,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -2217,6 +2423,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -2249,8 +2456,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2283,8 +2490,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2328,11 +2535,14 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2360,6 +2570,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2412,6 +2623,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2425,8 +2637,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2442,8 +2654,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2464,8 +2676,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2527,6 +2739,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -2543,8 +2756,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2629,8 +2842,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= -google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2780,12 +2993,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= -google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2830,8 +3043,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2864,11 +3077,14 @@ gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD79kk= gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -2877,14 +3093,15 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2897,18 +3114,18 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= istio.io/pkg v0.0.0-20231221211216-7635388a563e h1:ZlLVbKDlCzfP0MPbWc6VRcY23d9NdjLxwpPQpDrh3Gc= istio.io/pkg v0.0.0-20231221211216-7635388a563e/go.mod h1:fvmqEdHhZjYYwf6dSiIwvwc7db54kMWVTfsb91KmhzY= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/cri-api v0.35.0 h1:fxLSKyJHqbyCSUsg1rW4DRpmjSEM/elZ1GXzYTSLoDQ= @@ -3005,6 +3222,8 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099Yo sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= diff --git a/pkg/config/config.go b/pkg/config/config.go index 3af844815a..40eecfea01 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,6 +66,7 @@ type Config struct { EnableRuntimeDetection bool `mapstructure:"runtimeDetectionEnabled"` EnableSbomGeneration bool `mapstructure:"sbomGenerationEnabled"` EnableSeccomp bool `mapstructure:"seccompServiceEnabled"` + EnableSignatureVerification bool `mapstructure:"enableSignatureVerification"` HostMonitoringEnabled bool `mapstructure:"hostMonitoringEnabled"` StandaloneMonitoringEnabled bool `mapstructure:"standaloneMonitoringEnabled"` SeccompProfileBackend string `mapstructure:"seccompProfileBackend"` @@ -182,6 +183,7 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("celConfigCache::maxSize", 100000) viper.SetDefault("celConfigCache::ttl", 1*time.Minute) viper.SetDefault("ignoreRuleBindings", false) + viper.SetDefault("enableSignatureVerification", false) viper.SetDefault("dnsCacheSize", 50000) viper.SetDefault("seccompProfileBackend", "storage") // "storage" or "crd" @@ -214,6 +216,7 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("hostSensorInterval", 5*time.Minute) viper.AutomaticEnv() + _ = viper.BindEnv("enableSignatureVerification", "ENABLE_SIGNATURE_VERIFICATION") err := viper.ReadInConfig() if err != nil { diff --git a/pkg/containerprofilemanager/v1/lifecycle.go b/pkg/containerprofilemanager/v1/lifecycle.go index 8e40fd8702..44b689600b 100644 --- a/pkg/containerprofilemanager/v1/lifecycle.go +++ b/pkg/containerprofilemanager/v1/lifecycle.go @@ -93,14 +93,17 @@ func (cpm *ContainerProfileManager) addContainer(container *containercollection. return fmt.Errorf("failed to get shared data for container %s: %w", containerID, err) } - // Check if the container should use a user-defined profile + // Check if the container should use a user-defined profile. + // When both an ApplicationProfile and a NetworkNeighborhood are + // user-provided, skip ALL recording — there is nothing to learn. if sharedData.UserDefinedProfile != "" { logger.L().Debug("ignoring container with a user-defined profile", helpers.String("containerID", containerID), helpers.String("containerName", container.Runtime.ContainerName), helpers.String("podName", container.K8s.PodName), helpers.String("namespace", container.K8s.Namespace), - helpers.String("userDefinedProfile", sharedData.UserDefinedProfile)) + helpers.String("userDefinedProfile", sharedData.UserDefinedProfile), + helpers.String("userDefinedNetwork", sharedData.UserDefinedNetwork)) // Close ready channel before removing entry if entry, exists := cpm.getContainerEntry(containerID); exists { entry.readyOnce.Do(func() { diff --git a/pkg/ebpf/gadgets/randomx/program.bpf.c b/pkg/ebpf/gadgets/randomx/program.bpf.c index ed9cd5812b..46e7425e29 100644 --- a/pkg/ebpf/gadgets/randomx/program.bpf.c +++ b/pkg/ebpf/gadgets/randomx/program.bpf.c @@ -166,4 +166,176 @@ int tracepoint__x86_fpu_regs_deactivated(struct trace_event_raw_x86_fpu *ctx) char LICENSE[] SEC("license") = "GPL"; -#endif // defined(__TARGET_ARCH_x86) \ No newline at end of file +#endif // defined(__TARGET_ARCH_x86) + +/* // Kernel types definitions +#include + +// eBPF helpers signatures +// Check https://man7.org/linux/man-pages/man7/bpf-helpers.7.html to learn +// more about different available helpers +#include +#include + +// Inspektor Gadget buffer +#include +// Helpers to handle common data +#include +// Inspektor Gadget macros +#include +// Inspektor Gadget filtering +#include +// Inspektor Gadget types +#include +// Inspektor Gadget mntns +#include + +#include "program.h" +#include "upper_layer.h" +#include "exe_path.h" + +#if defined(__TARGET_ARCH_x86) + +#define TARGET_RANDOMX_EVENTS_COUNT 5 +// 5 seconds in nanoseconds +#define MAX_NS_BETWEEN_EVENTS 5000000000ULL + +// This struct will hold the state for each mount namespace +struct mntns_cache { + u64 timestamp; + u64 events_count; + bool alerted; +}; + +// A map to store the cache per mntns_id. +// key: mntns_id (u64), value: struct mntns_cache +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 1024); + __type(key, u64); + __type(value, struct mntns_cache); +} mntns_event_count SEC(".maps"); + +// events is the name of the buffer map and 1024 * 256 (256KB) is its size. +GADGET_TRACER_MAP(events, 1024 * 256); + +// Define a tracer +GADGET_TRACER(randomx, events, event); + +// Utilize the kernel version provided by libbpf. (kconfig must be present). +extern int LINUX_KERNEL_VERSION __kconfig; + +#if LINUX_KERNEL_VERSION <= KERNEL_VERSION(5, 15, 0) +struct old_fpu { + unsigned int last_cpu; + unsigned char initialized; + long: 24; + long: 64; + long: 64; + long: 64; + long: 64; + long: 64; + long: 64; + long: 64; + union fpregs_state state; +}; +#endif + +SEC("tracepoint/x86_fpu/x86_fpu_regs_deactivated") +int tracepoint__x86_fpu_regs_deactivated(struct trace_event_raw_x86_fpu *ctx) +{ + if (gadget_should_discard_data_current()) { + return 0; + } + + u64 mntns_id; + mntns_id = gadget_get_current_mntns_id(); + struct mntns_cache *cache; + cache = bpf_map_lookup_elem(&mntns_event_count, &mntns_id); + + u64 now = bpf_ktime_get_ns(); + + if (!cache) { + // First event for this mntns. Create a new entry. + struct mntns_cache new_cache = {}; + new_cache.timestamp = now; + new_cache.events_count = 1; + new_cache.alerted = false; + bpf_map_update_elem(&mntns_event_count, &mntns_id, &new_cache, BPF_ANY); + return 0; // Don't send an event yet + } + + // If we have already sent an alert for this mntns, do nothing. + if (cache->alerted) { + return 0; + } + + // Check if the last event was too long ago and reset if necessary. + if (now - cache->timestamp > MAX_NS_BETWEEN_EVENTS) { + cache->timestamp = now; + cache->events_count = 1; + bpf_map_update_elem(&mntns_event_count, &mntns_id, cache, BPF_ANY); + return 0; // Don't send an event yet + } + + // Increment the count. Using bpf_map_update_elem is not atomic, but for + // this use case (a single CPU tracepoint), it's safe. + cache->events_count++; + cache->timestamp = now; // Update timestamp with the latest event + + // Check if we have seen enough events + if (cache->events_count <= TARGET_RANDOMX_EVENTS_COUNT) { + // Not enough events yet, just update the map and exit. + bpf_map_update_elem(&mntns_event_count, &mntns_id, cache, BPF_ANY); + return 0; + } + + // --- Threshold has been reached! --- + // We only reach this point ONCE per mntns. + + // Mark as alerted to prevent sending more events for this mntns. + cache->alerted = true; + bpf_map_update_elem(&mntns_event_count, &mntns_id, cache, BPF_ANY); + + struct event *event; + event = gadget_reserve_buf(&events, sizeof(*event)); + if (!event) { + return 0; + } + + // Populate the event with data. This code is the same as before. + gadget_process_populate(&event->proc); + + void *fpu = BPF_CORE_READ(ctx, fpu); + if (fpu == NULL) { + gadget_discard_buf(event); + return 0; + } + + u32 mxcsr; + if(LINUX_KERNEL_VERSION <= KERNEL_VERSION(5, 15, 0)) { + bpf_probe_read_kernel(&mxcsr, sizeof(mxcsr), &((struct old_fpu*)fpu)->state.xsave.i387.mxcsr); + } else { + mxcsr = BPF_CORE_READ((struct fpu*)fpu, fpstate, regs.xsave.i387.mxcsr); + } + + int fpcr = (mxcsr & 0x6000) >> 13; + if (fpcr != 0) { + event->upper_layer = has_upper_layer(); + read_exe_path(event->exepath, sizeof(event->exepath)); + + event->timestamp_raw = bpf_ktime_get_boot_ns(); + + gadget_submit_buf(ctx, &events, event, sizeof(*event)); + } else { + gadget_discard_buf(event); + } + + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; + +#endif // defined(__TARGET_ARCH_x86) + + */ \ No newline at end of file diff --git a/pkg/objectcache/applicationprofilecache/applicationprofilecache.go b/pkg/objectcache/applicationprofilecache/applicationprofilecache.go index c159ee8675..ffcd145fbc 100644 --- a/pkg/objectcache/applicationprofilecache/applicationprofilecache.go +++ b/pkg/objectcache/applicationprofilecache/applicationprofilecache.go @@ -14,10 +14,15 @@ import ( "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/armosec/armoapi-go/armotypes" "github.com/kubescape/node-agent/pkg/config" + "github.com/kubescape/node-agent/pkg/exporters" "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/objectcache/applicationprofilecache/callstackcache" "github.com/kubescape/node-agent/pkg/resourcelocks" + "github.com/kubescape/node-agent/pkg/rulemanager/types" + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/node-agent/pkg/signature/profiles" "github.com/kubescape/node-agent/pkg/storage" "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -48,6 +53,7 @@ type ApplicationProfileCacheImpl struct { containerToCallStackIndex maps.SafeMap[string, *ContainerCallStackIndex] storageClient storage.ProfileClient k8sObjectCache objectcache.K8sObjectCache + exporter exporters.Exporter // Exporter for sending tamper detection alerts updateInterval time.Duration updateInProgress bool // Flag to track if update is in progress updateMutex sync.Mutex // Mutex to protect the flag @@ -55,7 +61,7 @@ type ApplicationProfileCacheImpl struct { } // NewApplicationProfileCache creates a new application profile cache with periodic updates -func NewApplicationProfileCache(cfg config.Config, storageClient storage.ProfileClient, k8sObjectCache objectcache.K8sObjectCache) *ApplicationProfileCacheImpl { +func NewApplicationProfileCache(cfg config.Config, storageClient storage.ProfileClient, k8sObjectCache objectcache.K8sObjectCache, exporter exporters.Exporter) *ApplicationProfileCacheImpl { updateInterval := utils.AddJitter(cfg.ProfilesCacheRefreshRate, 10) // Add 10% jitter to avoid high load on the storage apc := &ApplicationProfileCacheImpl{ @@ -67,6 +73,7 @@ func NewApplicationProfileCache(cfg config.Config, storageClient storage.Profile containerToCallStackIndex: maps.SafeMap[string, *ContainerCallStackIndex]{}, storageClient: storageClient, k8sObjectCache: k8sObjectCache, + exporter: exporter, updateInterval: updateInterval, containerLocks: resourcelocks.New(), } @@ -244,6 +251,12 @@ func (apc *ApplicationProfileCacheImpl) updateAllProfiles(ctx context.Context) { continue } + // Verify signature if enabled + if err := apc.verifyApplicationProfile(fullProfile, workloadID, "profile", true); err != nil { + // Continue to next profile as per requirements: skip on verification failure + continue + } + apc.workloadIDToProfile.Set(workloadID, fullProfile) logger.L().Debug("updated profile in cache", helpers.String("workloadID", workloadID), @@ -265,6 +278,111 @@ func (apc *ApplicationProfileCacheImpl) updateAllProfiles(ctx context.Context) { } } +// verifyApplicationProfile verifies the profile signature. +// Always checks signed profiles for tamper (emits R1016 alert on tamper). +// When EnableSignatureVerification is true, also rejects tampered/unsigned profiles. +// Returns error if the profile should not be loaded, nil otherwise. +func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta1.ApplicationProfile, workloadID, context string, recordFailure bool) error { + profileAdapter := profiles.NewApplicationProfileAdapter(profile) + + // Always check signed profiles for tamper, regardless of enforcement setting + if signature.IsSigned(profileAdapter) { + if err := signature.VerifyObject(profileAdapter); err != nil { + // Signed profile failed verification → tamper detected + logger.L().Warning(context+" signature verification failed (tamper detected)", + helpers.String("profile", profile.Name), + helpers.String("namespace", profile.Namespace), + helpers.String("workloadID", workloadID), + helpers.Error(err)) + + // Emit R1016 tamper alert + apc.emitTamperAlert(profile.Name, profile.Namespace, workloadID, "ApplicationProfile", err) + + if apc.cfg.EnableSignatureVerification { + if recordFailure { + apc.setVerificationFailed(workloadID, profile.Name, err) + } + return err + } + // Enforcement off: allow loading despite tamper + return nil + } + logger.L().Debug(context+" verification successful", + helpers.String("profile", profile.Name), + helpers.String("namespace", profile.Namespace)) + return nil + } + + // Profile is not signed + if apc.cfg.EnableSignatureVerification { + logger.L().Debug(context+" is not signed, skipping", + helpers.String("profile", profile.Name), + helpers.String("namespace", profile.Namespace), + helpers.String("workloadID", workloadID)) + if recordFailure { + apc.setVerificationFailed(workloadID, profile.Name, signature.ErrObjectNotSigned) + } + return signature.ErrObjectNotSigned + } + + return nil +} + +// emitTamperAlert sends an R1016 "Signed profile tampered" alert via the exporter. +func (apc *ApplicationProfileCacheImpl) emitTamperAlert(profileName, namespace, workloadID, objectKind string, verifyErr error) { + if apc.exporter == nil { + return + } + + ruleFailure := &types.GenericRuleFailure{ + BaseRuntimeAlert: armotypes.BaseRuntimeAlert{ + AlertName: "Signed profile tampered", + InfectedPID: 1, + Severity: 10, + FixSuggestions: "Investigate who modified the " + objectKind + " '" + profileName + "' in namespace '" + namespace + "'. Re-sign the profile after verifying its contents.", + }, + AlertType: armotypes.AlertTypeRule, + RuntimeProcessDetails: armotypes.ProcessTree{ + ProcessTree: armotypes.Process{ + PID: 1, + Comm: "node-agent", + }, + }, + RuleAlert: armotypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Signed %s '%s' in namespace '%s' has been tampered with: %v", objectKind, profileName, namespace, verifyErr), + }, + RuntimeAlertK8sDetails: armotypes.RuntimeAlertK8sDetails{ + Namespace: namespace, + }, + RuleID: "R1016", + } + + // Populate workload details from workloadID if available + ruleFailure.SetWorkloadDetails(extractWlidFromWorkloadID(workloadID)) + + apc.exporter.SendRuleAlert(ruleFailure) +} + +// extractWlidFromWorkloadID extracts the wlid part from a "wlid/templateHash" key. +func extractWlidFromWorkloadID(workloadID string) string { + if idx := strings.LastIndex(workloadID, "/"); idx > 0 { + // workloadID format is "wlid://////" + // We need everything before the last "/" which is the templateHash + return workloadID[:idx] + } + return workloadID +} + +func (apc *ApplicationProfileCacheImpl) setVerificationFailed(workloadID, profileName string, err error) { + profileState := &objectcache.ProfileState{ + Completion: "failed", + Status: "verification-failed", + Name: profileName, + Error: err, + } + apc.workloadIDToProfileState.Set(workloadID, profileState) +} + // handleUserManagedProfile handles user-managed profiles func (apc *ApplicationProfileCacheImpl) handleUserManagedProfile(profile *v1beta1.ApplicationProfile) { normalizedProfileName := strings.TrimPrefix(profile.Name, helpersv1.UserApplicationProfilePrefix) @@ -314,6 +432,11 @@ func (apc *ApplicationProfileCacheImpl) handleUserManagedProfile(profile *v1beta return } + // Verify signature if enabled + if err := apc.verifyApplicationProfile(fullUserProfile, toMerge.wlid, "user-managed profile", false); err != nil { + return + } + // Merge the user-managed profile with the normal profile // First, pull the original profile from the storage @@ -497,7 +620,9 @@ func (apc *ApplicationProfileCacheImpl) addContainer(container *containercollect } } } else { - apc.workloadIDToProfileState.Set(workloadID, nil) + apc.workloadIDToProfileState.Set(workloadID, &objectcache.ProfileState{ + Error: fmt.Errorf("waiting for profile update"), + }) } // Create container info @@ -533,8 +658,26 @@ func (apc *ApplicationProfileCacheImpl) addContainer(container *containercollect apc.workloadIDToProfileState.Set(workloadID, profileState) return nil } + + // Verify signature if enabled + if err := apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile", false); err != nil { + // Update the profile state to indicate an error + profileState := &objectcache.ProfileState{ + Error: fmt.Errorf("signature verification failed: %w", err), + } + apc.workloadIDToProfileState.Set(workloadID, profileState) + // Skip caching the unverified profile + return nil + } + // Update the profile in the cache apc.workloadIDToProfile.Set(workloadID, fullProfile) + profileState := &objectcache.ProfileState{ + Completion: fullProfile.Annotations[helpersv1.CompletionMetadataKey], + Status: fullProfile.Annotations[helpersv1.StatusMetadataKey], + Name: fullProfile.Name, + } + apc.workloadIDToProfileState.Set(workloadID, profileState) logger.L().Debug("added user-defined profile to cache", helpers.String("containerID", containerID), helpers.String("workloadID", workloadID), @@ -710,10 +853,9 @@ func (apc *ApplicationProfileCacheImpl) GetApplicationProfileState(containerID s if profileState, exists := apc.workloadIDToProfileState.Load(workloadID); exists { if profileState != nil { return profileState - } else { - return &objectcache.ProfileState{ - Error: fmt.Errorf("profile state not available - shouldn't happen"), - } + } + return &objectcache.ProfileState{ + Error: fmt.Errorf("application profile state is nil for workload %s", workloadID), } } diff --git a/pkg/objectcache/applicationprofilecache/applicationprofilecache_test.go b/pkg/objectcache/applicationprofilecache/applicationprofilecache_test.go index 7ce56181c7..6a89edcb24 100644 --- a/pkg/objectcache/applicationprofilecache/applicationprofilecache_test.go +++ b/pkg/objectcache/applicationprofilecache/applicationprofilecache_test.go @@ -77,7 +77,7 @@ func TestPagination(t *testing.T) { spy := &SpyProfileClient{Profiles: profiles} // mock k8s object cache is irrelevant since we inject container info directly - cache := NewApplicationProfileCache(config.Config{}, spy, nil) + cache := NewApplicationProfileCache(config.Config{}, spy, nil, nil) // Inject a container so that "default" namespace is processed. // The WorkloadID needs to match something if we want deeper logic to run, diff --git a/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go b/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go index 254f6ae640..6efd6042a1 100644 --- a/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go +++ b/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go @@ -14,9 +14,14 @@ import ( "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/armosec/armoapi-go/armotypes" "github.com/kubescape/node-agent/pkg/config" + "github.com/kubescape/node-agent/pkg/exporters" "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/resourcelocks" + "github.com/kubescape/node-agent/pkg/rulemanager/types" + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/node-agent/pkg/signature/profiles" "github.com/kubescape/node-agent/pkg/storage" "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -30,6 +35,7 @@ type ContainerInfo struct { InstanceTemplateHash string Namespace string SeenContainerFromTheStart bool // True if container was seen from the start + UserDefinedNetwork string // Non-empty when pod has a user-defined NN label } // NetworkNeighborhoodCacheImpl implements the NetworkNeighborhoodCache interface @@ -41,6 +47,7 @@ type NetworkNeighborhoodCacheImpl struct { networkNeighborhoodToUserManagedIdentifier maps.SafeMap[string, string] // networkNeighborhoodName -> user-managed profile unique identifier storageClient storage.ProfileClient k8sObjectCache objectcache.K8sObjectCache + exporter exporters.Exporter // Exporter for sending tamper detection alerts updateInterval time.Duration updateInProgress bool // Flag to track if update is in progress updateMutex sync.Mutex // Mutex to protect the flag @@ -48,7 +55,7 @@ type NetworkNeighborhoodCacheImpl struct { } // NewNetworkNeighborhoodCache creates a new network neighborhood cache with periodic updates -func NewNetworkNeighborhoodCache(cfg config.Config, storageClient storage.ProfileClient, k8sObjectCache objectcache.K8sObjectCache) *NetworkNeighborhoodCacheImpl { +func NewNetworkNeighborhoodCache(cfg config.Config, storageClient storage.ProfileClient, k8sObjectCache objectcache.K8sObjectCache, exporter exporters.Exporter) *NetworkNeighborhoodCacheImpl { updateInterval := utils.AddJitter(cfg.ProfilesCacheRefreshRate, 10) // Add 10% jitter to avoid high load on the storage nnc := &NetworkNeighborhoodCacheImpl{ @@ -59,6 +66,7 @@ func NewNetworkNeighborhoodCache(cfg config.Config, storageClient storage.Profil networkNeighborhoodToUserManagedIdentifier: maps.SafeMap[string, string]{}, storageClient: storageClient, k8sObjectCache: k8sObjectCache, + exporter: exporter, updateInterval: updateInterval, containerLocks: resourcelocks.New(), } @@ -204,6 +212,13 @@ func (nnc *NetworkNeighborhoodCacheImpl) updateAllNetworkNeighborhoods(ctx conte continue } + // Never overwrite a user-defined network neighborhood with an + // auto-learned one. Check if any container for this workload + // has a user-defined-network label. + if nnc.workloadHasUserDefinedNetwork(workloadID) { + continue + } + // If we have a "new" container (seen from start) and the network neighborhood is partial, // skip it - we don't want to use partial profiles for containers we're tracking from the start if hasNewContainer && nn.Annotations[helpersv1.CompletionMetadataKey] == helpersv1.Partial { @@ -238,6 +253,14 @@ func (nnc *NetworkNeighborhoodCacheImpl) updateAllNetworkNeighborhoods(ctx conte continue } + // Verify signature — always check signed NNs for tamper (R1016), + // enforcement mode only controls whether tampered NNs are loaded. + if err := nnc.verifyNetworkNeighborhood(fullNN, workloadID); err != nil { + profileState.Error = fmt.Errorf("signature verification failed: %w", err) + nnc.workloadIDToProfileState.Set(workloadID, profileState) + continue + } + nnc.workloadIDToNetworkNeighborhood.Set(workloadID, fullNN) logger.L().Debug("updated network neighborhood in cache", helpers.String("workloadID", workloadID), @@ -308,8 +331,46 @@ func (nnc *NetworkNeighborhoodCacheImpl) handleUserManagedNetworkNeighborhood(nn helpers.Error(err)) return } + + // Verify signature on the original network neighborhood before merging + if err := nnc.verifyNetworkNeighborhood(originalNN, toMerge.wlid); err != nil { + profileState := &objectcache.ProfileState{ + Completion: originalNN.Annotations[helpersv1.CompletionMetadataKey], + Status: originalNN.Annotations[helpersv1.StatusMetadataKey], + Name: originalNN.Name, + Error: fmt.Errorf("signature verification failed: %w", err), + } + nnc.workloadIDToProfileState.Set(toMerge.wlid, profileState) + // Evict stale merged profile from cache on verification failure + nnc.workloadIDToNetworkNeighborhood.Delete(toMerge.wlid) + return + } + + // Verify signature on the user-managed network neighborhood before merging + if err := nnc.verifyNetworkNeighborhood(fullUserNN, toMerge.wlid); err != nil { + profileState := &objectcache.ProfileState{ + Completion: fullUserNN.Annotations[helpersv1.CompletionMetadataKey], + Status: fullUserNN.Annotations[helpersv1.StatusMetadataKey], + Name: fullUserNN.Name, + Error: fmt.Errorf("signature verification failed: %w", err), + } + nnc.workloadIDToProfileState.Set(toMerge.wlid, profileState) + // Restore cache to originalNN on user-managed verification failure + nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, originalNN) + return + } + // Merge the network neighborhoods mergedNN := nnc.performMerge(originalNN, fullUserNN) + + // Clear stale signature annotations after merge + delete(mergedNN.Annotations, signature.AnnotationSignature) + delete(mergedNN.Annotations, signature.AnnotationCertificate) + delete(mergedNN.Annotations, signature.AnnotationRekorBundle) + delete(mergedNN.Annotations, signature.AnnotationIssuer) + delete(mergedNN.Annotations, signature.AnnotationIdentity) + delete(mergedNN.Annotations, signature.AnnotationTimestamp) + // Update the cache with the merged network neighborhood nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN) // Update profile state for the merged profile @@ -419,14 +480,53 @@ func (nnc *NetworkNeighborhoodCacheImpl) addContainer(container *containercollec InstanceTemplateHash: sharedData.InstanceID.GetTemplateHash(), Namespace: container.K8s.Namespace, SeenContainerFromTheStart: !sharedData.PreRunningContainer, + UserDefinedNetwork: sharedData.UserDefinedNetwork, } // Add to container info map nnc.containerIDToInfo.Set(containerID, containerInfo) + // If the container has a user-defined network neighborhood, load it + // directly into the cache — skip learning entirely for this workload. + if sharedData.UserDefinedNetwork != "" { + fullNN, err := nnc.storageClient.GetNetworkNeighborhood( + container.K8s.Namespace, sharedData.UserDefinedNetwork) + if err != nil { + logger.L().Error("failed to get user-defined network neighborhood", + helpers.String("containerID", containerID), + helpers.String("workloadID", workloadID), + helpers.String("namespace", container.K8s.Namespace), + helpers.String("nnName", sharedData.UserDefinedNetwork), + helpers.Error(err)) + profileState := &objectcache.ProfileState{ + Error: err, + } + nnc.workloadIDToProfileState.Set(workloadID, profileState) + return nil + } + + nnc.workloadIDToNetworkNeighborhood.Set(workloadID, fullNN) + profileState := &objectcache.ProfileState{ + Completion: helpersv1.Full, + Status: helpersv1.Completed, + Name: fullNN.Name, + Error: nil, + } + nnc.workloadIDToProfileState.Set(workloadID, profileState) + + logger.L().Debug("added user-defined network neighborhood to cache", + helpers.String("containerID", containerID), + helpers.String("workloadID", workloadID), + helpers.String("namespace", container.K8s.Namespace), + helpers.String("nnName", sharedData.UserDefinedNetwork)) + return nil + } + // Create workload ID to state mapping if _, exists := nnc.workloadIDToProfileState.Load(workloadID); !exists { - nnc.workloadIDToProfileState.Set(workloadID, nil) + nnc.workloadIDToProfileState.Set(workloadID, &objectcache.ProfileState{ + Error: fmt.Errorf("waiting for profile update"), + }) } logger.L().Debug("container added to cache", @@ -537,10 +637,9 @@ func (nnc *NetworkNeighborhoodCacheImpl) GetNetworkNeighborhoodState(containerID if profileState, exists := nnc.workloadIDToProfileState.Load(workloadID); exists { if profileState != nil { return profileState - } else { - return &objectcache.ProfileState{ - Error: fmt.Errorf("profile state not available - shouldn't happen"), - } + } + return &objectcache.ProfileState{ + Error: fmt.Errorf("network neighborhood state is nil for workload %s", workloadID), } } @@ -718,6 +817,98 @@ func (nnc *NetworkNeighborhoodCacheImpl) mergeNetworkPorts(normalPorts, userPort return normalPorts } +// workloadHasUserDefinedNetwork returns true if any container tracked for +// the given workloadID has a user-defined-network label set. +func (nnc *NetworkNeighborhoodCacheImpl) workloadHasUserDefinedNetwork(workloadID string) bool { + found := false + nnc.containerIDToInfo.Range(func(_ string, info *ContainerInfo) bool { + if info.WorkloadID == workloadID && info.UserDefinedNetwork != "" { + found = true + return false // stop iteration + } + return true + }) + return found +} + +// verifyNetworkNeighborhood verifies the NN signature. +// Always checks signed NNs for tamper (emits R1016 alert on tamper). +// When EnableSignatureVerification is true, also rejects tampered/unsigned NNs. +// Returns error if the NN should not be loaded, nil otherwise. +func (nnc *NetworkNeighborhoodCacheImpl) verifyNetworkNeighborhood(nn *v1beta1.NetworkNeighborhood, workloadID string) error { + adapter := profiles.NewNetworkNeighborhoodAdapter(nn) + + // Always check signed NNs for tamper, regardless of enforcement setting + if signature.IsSigned(adapter) { + if err := signature.VerifyObjectStrict(adapter); err != nil { + logger.L().Warning("network neighborhood signature verification failed (tamper detected)", + helpers.String("name", nn.Name), + helpers.String("namespace", nn.Namespace), + helpers.String("workloadID", workloadID), + helpers.Error(err)) + + // Emit R1016 tamper alert + nnc.emitTamperAlert(nn.Name, nn.Namespace, workloadID, "NetworkNeighborhood", err) + + if nnc.cfg.EnableSignatureVerification { + return err + } + // Enforcement off: allow loading despite tamper + return nil + } + return nil + } + + // Not signed + if nnc.cfg.EnableSignatureVerification { + return fmt.Errorf("network neighborhood is not signed") + } + return nil +} + +// emitTamperAlert sends an R1016 "Signed profile tampered" alert via the exporter. +func (nnc *NetworkNeighborhoodCacheImpl) emitTamperAlert(nnName, namespace, workloadID, objectKind string, verifyErr error) { + if nnc.exporter == nil { + return + } + + ruleFailure := &types.GenericRuleFailure{ + BaseRuntimeAlert: armotypes.BaseRuntimeAlert{ + AlertName: "Signed profile tampered", + InfectedPID: 1, + Severity: 10, + FixSuggestions: "Investigate who modified the " + objectKind + " '" + nnName + "' in namespace '" + namespace + "'. Re-sign the profile after verifying its contents.", + }, + AlertType: armotypes.AlertTypeRule, + RuntimeProcessDetails: armotypes.ProcessTree{ + ProcessTree: armotypes.Process{ + PID: 1, + Comm: "node-agent", + }, + }, + RuleAlert: armotypes.RuleAlert{ + RuleDescription: fmt.Sprintf("Signed %s '%s' in namespace '%s' has been tampered with: %v", objectKind, nnName, namespace, verifyErr), + }, + RuntimeAlertK8sDetails: armotypes.RuntimeAlertK8sDetails{ + Namespace: namespace, + }, + RuleID: "R1016", + } + + // Populate workload details from workloadID if available + ruleFailure.SetWorkloadDetails(extractWlidFromWorkloadID(workloadID)) + + nnc.exporter.SendRuleAlert(ruleFailure) +} + +// extractWlidFromWorkloadID extracts the wlid part from a "wlid/templateHash" key. +func extractWlidFromWorkloadID(workloadID string) string { + if idx := strings.LastIndex(workloadID, "/"); idx > 0 { + return workloadID[:idx] + } + return workloadID +} + func isUserManagedNN(nn *v1beta1.NetworkNeighborhood) bool { return nn.Annotations != nil && nn.Annotations[helpersv1.ManagedByMetadataKey] == helpersv1.ManagedByUserValue && diff --git a/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache_test.go b/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache_test.go index f2714141cb..47ea2097e5 100644 --- a/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache_test.go +++ b/pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache_test.go @@ -80,7 +80,7 @@ func TestPagination(t *testing.T) { spy := &SpyProfileClient{NetworkNeighborhoods: items} - cache := NewNetworkNeighborhoodCache(config.Config{}, spy, nil) + cache := NewNetworkNeighborhoodCache(config.Config{}, spy, nil, nil) // Inject a container so that "default" namespace is processed. cache.containerIDToInfo.Set("test-container", &ContainerInfo{ diff --git a/pkg/objectcache/shared_container_data.go b/pkg/objectcache/shared_container_data.go index cf0e7d2b4d..87952dbe45 100644 --- a/pkg/objectcache/shared_container_data.go +++ b/pkg/objectcache/shared_container_data.go @@ -20,6 +20,11 @@ import ( "k8s.io/apimachinery/pkg/util/validation" ) +// UserDefinedNetworkMetadataKey is the pod label that references a +// user-provided NetworkNeighborhood resource by name (analogous to +// helpersv1.UserDefinedProfileMetadataKey for ApplicationProfiles). +const UserDefinedNetworkMetadataKey = "kubescape.io/user-defined-network" + type ContainerType int const ( @@ -82,6 +87,7 @@ type WatchedContainerData struct { PreviousReportTimestamp time.Time CurrentReportTimestamp time.Time UserDefinedProfile string + UserDefinedNetwork string } type ContainerInfo struct { @@ -167,6 +173,16 @@ func (watchedContainer *WatchedContainerData) SetContainerInfo(wl workloadinterf watchedContainer.UserDefinedProfile = userDefinedProfile } } + // check for user defined network neighborhood + if userDefinedNetwork, ok := labels[UserDefinedNetworkMetadataKey]; ok { + if userDefinedNetwork != "" { + logger.L().Info("container has a user defined network neighborhood", + helpers.String("network", userDefinedNetwork), + helpers.String("container", containerName), + helpers.String("workload", wl.GetName())) + watchedContainer.UserDefinedNetwork = userDefinedNetwork + } + } podSpec, err := wl.GetPodSpec() if err != nil { return fmt.Errorf("failed to get pod spec: %w", err) diff --git a/pkg/rulemanager/ruleswatcher/watcher.go b/pkg/rulemanager/ruleswatcher/watcher.go index 45782beb23..9d4c4b003e 100644 --- a/pkg/rulemanager/ruleswatcher/watcher.go +++ b/pkg/rulemanager/ruleswatcher/watcher.go @@ -2,14 +2,18 @@ package ruleswatcher import ( "context" + "errors" "os" "github.com/Masterminds/semver/v3" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/node-agent/pkg/config" "github.com/kubescape/node-agent/pkg/k8sclient" "github.com/kubescape/node-agent/pkg/rulemanager/rulecreator" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/node-agent/pkg/signature/profiles" "github.com/kubescape/node-agent/pkg/watcher" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -22,14 +26,16 @@ var _ RulesWatcher = (*RulesWatcherImpl)(nil) type RulesWatcherImpl struct { ruleCreator rulecreator.RuleCreator k8sClient k8sclient.K8sClientInterface + cfg *config.Config callback RulesWatcherCallback watchResources []watcher.WatchResource } -func NewRulesWatcher(k8sClient k8sclient.K8sClientInterface, ruleCreator rulecreator.RuleCreator, callback RulesWatcherCallback) *RulesWatcherImpl { +func NewRulesWatcher(k8sClient k8sclient.K8sClientInterface, ruleCreator rulecreator.RuleCreator, callback RulesWatcherCallback, cfg *config.Config) *RulesWatcherImpl { return &RulesWatcherImpl{ ruleCreator: ruleCreator, k8sClient: k8sClient, + cfg: cfg, callback: callback, watchResources: []watcher.WatchResource{ watcher.NewWatchResource(typesv1.RuleGvr, metav1.ListOptions{}), @@ -71,7 +77,8 @@ func (w *RulesWatcherImpl) syncAllRulesAndNotify(ctx context.Context) { // syncAllRulesFromCluster fetches all rules from the cluster and syncs them with the rule creator. // Rules are filtered by: // 1. Enabled status - only enabled rules are considered -// 2. Agent version compatibility - rules with AgentVersionRequirement are checked against AGENT_VERSION env var using semver +// 2. Signature verification - if enabled, verifies rules have valid signatures +// 3. Agent version compatibility - rules with AgentVersionRequirement are checked against AGENT_VERSION env var using semver func (w *RulesWatcherImpl) syncAllRulesFromCluster(ctx context.Context) error { unstructuredList, err := w.k8sClient.GetDynamicClient().Resource(typesv1.RuleGvr).List(ctx, metav1.ListOptions{}) if err != nil { @@ -80,12 +87,20 @@ func (w *RulesWatcherImpl) syncAllRulesFromCluster(ctx context.Context) error { var enabledRules []typesv1.Rule var skippedVersionCount int + var skippedVerificationCount int for _, item := range unstructuredList.Items { rules, err := unstructuredToRules(&item) if err != nil { logger.L().Warning("RulesWatcher - failed to convert rule during sync", helpers.Error(err)) continue } + + // Verify signature if enabled + if err := w.verifyRules(rules); err != nil { + skippedVerificationCount++ + continue + } + for _, rule := range rules.Spec.Rules { if rule.Enabled { // Check agent version requirement if specified @@ -109,7 +124,8 @@ func (w *RulesWatcherImpl) syncAllRulesFromCluster(ctx context.Context) error { logger.L().Info("RulesWatcher - synced rules from cluster", helpers.Int("enabledRules", len(enabledRules)), helpers.Int("totalRules", len(unstructuredList.Items)), - helpers.Int("skippedByVersion", skippedVersionCount)) + helpers.Int("skippedByVersion", skippedVersionCount), + helpers.Int("skippedByVerification", skippedVerificationCount)) return nil } @@ -126,6 +142,30 @@ func unstructuredToRules(obj *unstructured.Unstructured) (*typesv1.Rules, error) return rule, nil } +func (w *RulesWatcherImpl) verifyRules(rules *typesv1.Rules) error { + if w.cfg == nil || !w.cfg.EnableSignatureVerification { + return nil + } + rulesAdapter := profiles.NewRulesAdapter(rules) + if err := signature.VerifyObject(rulesAdapter); err != nil { + if errors.Is(err, signature.ErrObjectNotSigned) { + logger.L().Debug("Rules resource is not signed, skipping", + helpers.String("name", rules.Name), + helpers.String("namespace", rules.Namespace)) + } else { + logger.L().Warning("Rules resource signature verification failed", + helpers.String("name", rules.Name), + helpers.String("namespace", rules.Namespace), + helpers.Error(err)) + } + return err + } + logger.L().Debug("Rules resource signature verification successful", + helpers.String("name", rules.Name), + helpers.String("namespace", rules.Namespace)) + return nil +} + // isAgentVersionCompatible checks if the current agent version satisfies the given requirement // using semantic versioning constraints. Returns true if compatible, false otherwise. func isAgentVersionCompatible(requirement string) bool { diff --git a/pkg/signature/annotations.go b/pkg/signature/annotations.go new file mode 100644 index 0000000000..8df333d21e --- /dev/null +++ b/pkg/signature/annotations.go @@ -0,0 +1,16 @@ +package signature + +import "errors" + +const ( + AnnotationPrefix = "signature.kubescape.io" + + AnnotationSignature = AnnotationPrefix + "/signature" + AnnotationCertificate = AnnotationPrefix + "/certificate" + AnnotationRekorBundle = AnnotationPrefix + "/rekor-bundle" + AnnotationIssuer = AnnotationPrefix + "/issuer" + AnnotationIdentity = AnnotationPrefix + "/identity" + AnnotationTimestamp = AnnotationPrefix + "/timestamp" +) + +var ErrObjectNotSigned = errors.New("object is not signed (missing signature annotation)") diff --git a/pkg/signature/cluster_flow_test.go b/pkg/signature/cluster_flow_test.go new file mode 100644 index 0000000000..23dfe89580 --- /dev/null +++ b/pkg/signature/cluster_flow_test.go @@ -0,0 +1,150 @@ +package signature + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "testing" + "time" + + sigstore_signature "github.com/sigstore/sigstore/pkg/signature" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kubescape/node-agent/pkg/signature/profiles" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" +) + +func TestClusterProfileStructure(t *testing.T) { + // Simulate a cluster profile with empty TypeMeta (like from cluster) + profile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "", + Kind: "", + }, + } + profile.Name = "test-signed" + profile.Namespace = "default" + + adapter := profiles.NewApplicationProfileAdapter(profile) + content := adapter.GetContent() + + if m, ok := content.(map[string]interface{}); ok { + t.Logf("apiVersion: %v (type: %T)", m["apiVersion"], m["apiVersion"]) + t.Logf("kind: %v (type: %T)", m["kind"], m["kind"]) + + // Verify fallback values are applied + if m["apiVersion"] != "spdx.softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected fallback apiVersion, got %s", m["apiVersion"]) + } + if m["kind"] != "ApplicationProfile" { + t.Errorf("Expected fallback kind, got %s", m["kind"]) + } + } else { + t.Errorf("Expected map, got %T", content) + } +} + +func TestReproduceClusterVerificationFlow(t *testing.T) { + // Simulate the exact scenario from the cluster + profile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "", + Kind: "", + }, + } + profile.Name = "replicaset-nginx2-5bffdcc777-signed" + profile.Namespace = "default" + profile.Labels = map[string]string{ + "kubescape.io/instance-template-hash": "5bffdcc777", + "kubescape.io/workload-api-group": "apps", + "kubescape.io/workload-api-version": "v1", + "kubescape.io/workload-kind": "Deployment", + "kubescape.io/workload-name": "nginx2", + "kubescape.io/workload-namespace": "default", + "kubescape.io/workload-resource-version": "15471", + } + + adapter := profiles.NewApplicationProfileAdapter(profile) + + // Calculate hash + cosignAdapter := &CosignAdapter{} + hash, err := cosignAdapter.GetContentHash(adapter.GetContent()) + if err != nil { + t.Fatalf("Failed to compute hash: %v", err) + } + + t.Logf("Computed hash: %s", hash) + + // Generate a key and sign + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("Failed to generate key: %v", err) + } + signer, err := sigstore_signature.LoadECDSASigner(privKey, crypto.SHA256) + if err != nil { + t.Fatalf("Failed to load signer: %v", err) + } + + sig, err := signer.SignMessage(bytes.NewReader([]byte(hash))) + if err != nil { + t.Fatalf("Failed to sign message: %v", err) + } + certBytes, err := generateTestCertificate(privKey) + if err != nil { + t.Fatalf("Failed to generate test certificate: %v", err) + } + + // Use the package-level annotation flow + sigObj := &Signature{ + Signature: sig, + Certificate: certBytes, + Timestamp: time.Now().Unix(), + } + annotations, err := cosignAdapter.EncodeSignatureToAnnotations(sigObj) + if err != nil { + t.Fatalf("Failed to encode signature to annotations: %v", err) + } + adapter.SetAnnotations(annotations) + + // Now verify using the higher-level flow + err = VerifyObjectAllowUntrusted(adapter) + if err != nil { + t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err) + } +} + +func generateTestCertificate(privKey *ecdsa.PrivateKey) ([]byte, error) { + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "test-signer", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + return nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }) + + return certPEM, nil +} diff --git a/pkg/signature/cluster_scenario_test.go b/pkg/signature/cluster_scenario_test.go new file mode 100644 index 0000000000..b26813d6d2 --- /dev/null +++ b/pkg/signature/cluster_scenario_test.go @@ -0,0 +1,88 @@ +package signature + +import ( + "testing" + + "github.com/kubescape/node-agent/pkg/signature/profiles" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// This test replicates the exact scenario from the production cluster where: +// 1. Profiles are loaded from the cluster with empty TypeMeta (APIVersion="", Kind="") +// 2. The adapter's GetContent() should fill in the correct fallback values +// 3. Signatures created and verified using these profiles should succeed + +func TestClusterScenarioIntegration(t *testing.T) { + // Simulate a profile as it comes from the cluster (empty TypeMeta) + clusterProfile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "", + Kind: "", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "replicaset-test-workload-123456789", + Namespace: "default", + Labels: map[string]string{ + "kubescape.io/instance-template-hash": "123456789", + "kubescape.io/workload-kind": "Deployment", + "kubescape.io/workload-name": "test-workload", + "kubescape.io/workload-namespace": "default", + }, + }, + } + + // Create adapter + adapter := profiles.NewApplicationProfileAdapter(clusterProfile) + + // Verify GetContent() populates TypeMeta correctly + content := adapter.GetContent() + contentMap, ok := content.(map[string]interface{}) + if !ok { + t.Fatalf("GetContent() should return map[string]interface{}, got %T", content) + } + + // Check that fallback values are applied + if contentMap["apiVersion"] != "spdx.softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected apiVersion fallback to be applied, got: %v", contentMap["apiVersion"]) + } + if contentMap["kind"] != "ApplicationProfile" { + t.Errorf("Expected kind fallback to be applied, got: %v", contentMap["kind"]) + } + + // Verify metadata is correctly structured + metadata, ok := contentMap["metadata"].(map[string]interface{}) + if !ok { + t.Fatal("metadata should be a map[string]interface{}") + } + + if metadata["name"] != clusterProfile.Name { + t.Errorf("Expected metadata.name=%s, got %v", clusterProfile.Name, metadata["name"]) + } + if metadata["namespace"] != clusterProfile.Namespace { + t.Errorf("Expected metadata.namespace=%s, got %v", clusterProfile.Namespace, metadata["namespace"]) + } + if metadata["labels"] == nil { + t.Error("metadata.labels should not be nil") + } + + // Now verify that signing and verification work end-to-end + if err := SignObjectDisableKeyless(adapter); err != nil { + t.Fatalf("Failed to sign object: %v", err) + } + + if clusterProfile.Annotations == nil { + t.Fatal("Annotations should be set after signing") + } + + if _, ok := clusterProfile.Annotations[AnnotationSignature]; !ok { + t.Error("Signature annotation should be set after signing") + } + + // Verify the signature + if err := VerifyObjectAllowUntrusted(adapter); err != nil { + t.Fatalf("Failed to verify object: %v", err) + } + + t.Log("✓ Cluster scenario integration test passed: profile with empty TypeMeta successfully signed and verified") +} diff --git a/pkg/signature/cosign_adapter.go b/pkg/signature/cosign_adapter.go new file mode 100644 index 0000000000..b78d8920ab --- /dev/null +++ b/pkg/signature/cosign_adapter.go @@ -0,0 +1,572 @@ +package signature + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "net/url" + "strconv" + "time" + + "context" + "github.com/golang-jwt/jwt/v5" + "github.com/kubescape/storage/pkg/utils" + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/pkg/cosign/bundle" + "github.com/sigstore/cosign/v3/pkg/providers" + _ "github.com/sigstore/cosign/v3/pkg/providers/all" + "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/fulcioroots" + "github.com/sigstore/sigstore/pkg/oauthflow" + sigstore_signature "github.com/sigstore/sigstore/pkg/signature" +) + +var _ = cosign.Signature +var _ = providers.Enabled +var _ = bundle.RekorBundle{} +var _ = api.CertificateRequest{} +var _ = client.Rekor{} +var _ = models.LogEntry{} +var _ = fulcioroots.Get +var _ = oauthflow.OIDConnect +var _ = oauthflow.DefaultIDTokenGetter + +const ( + sigstoreIssuer = "https://token.actions.githubusercontent.com" + sigstoreOIDC = "kubernetes.io" + fulcioURL = "https://fulcio.sigstore.dev" + rekorURL = "https://rekor.sigstore.dev" +) + +type CosignAdapter struct { + privateKey *ecdsa.PrivateKey + signer sigstore_signature.Signer + verifier sigstore_signature.Verifier + useKeyless bool + tokenProvider func(ctx context.Context) (string, error) +} + +func NewCosignAdapter(useKeyless bool) (*CosignAdapter, error) { + if useKeyless { + return &CosignAdapter{ + useKeyless: true, + }, nil + } + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %w", err) + } + + signer, err := sigstore_signature.LoadECDSASigner(privateKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to load ECDSA signer: %w", err) + } + + verifier, err := sigstore_signature.LoadECDSAVerifier(&privateKey.PublicKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to load ECDSA verifier: %w", err) + } + + return &CosignAdapter{ + privateKey: privateKey, + signer: signer, + verifier: verifier, + useKeyless: false, + }, nil +} + +func NewCosignAdapterWithPrivateKey(useKeyless bool, privateKey *ecdsa.PrivateKey) (*CosignAdapter, error) { + if privateKey == nil { + return nil, fmt.Errorf("private key cannot be nil") + } + + signer, err := sigstore_signature.LoadECDSASigner(privateKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to load ECDSA signer: %w", err) + } + + verifier, err := sigstore_signature.LoadECDSAVerifier(&privateKey.PublicKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to load ECDSA verifier: %w", err) + } + + return &CosignAdapter{ + privateKey: privateKey, + signer: signer, + verifier: verifier, + useKeyless: useKeyless, + }, nil +} + +func (c *CosignAdapter) SignData(data []byte) (*Signature, error) { + if c.useKeyless { + return c.signKeyless(data) + } + + return c.signWithKey(data) +} + +func (c *CosignAdapter) SetTokenProvider(provider func(context.Context) (string, error)) { + c.tokenProvider = provider +} + +func (c *CosignAdapter) signKeyless(data []byte) (*Signature, error) { + ctx := context.Background() + + var tok string + var err error + var identity string + var issuer string + + // 1. Get OIDC Token + if c.tokenProvider != nil { + tok, err = c.tokenProvider(ctx) + if err != nil { + return nil, fmt.Errorf("failed to provide OIDC token from provider: %w", err) + } + } else if providers.Enabled(ctx) { + tok, err = providers.Provide(ctx, "sigstore") + if err != nil { + return nil, fmt.Errorf("failed to provide OIDC token: %w", err) + } + } + + if tok != "" { + // Extract "sub" and "iss" from the JWT token + parser := jwt.NewParser() + token, _, err := parser.ParseUnverified(tok, jwt.MapClaims{}) + if err != nil { + return nil, fmt.Errorf("failed to parse OIDC token: %w", err) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("failed to get claims from OIDC token") + } + + sub, ok := claims["sub"].(string) + if !ok { + return nil, fmt.Errorf("failed to get 'sub' claim from OIDC token") + } + identity = sub + + iss, ok := claims["iss"].(string) + if !ok { + return nil, fmt.Errorf("failed to get 'iss' claim from OIDC token") + } + issuer = iss + } else { + // Fallback to interactive flow if not in CI and no provider + fmt.Println("No OIDC provider enabled (CI). Falling back to interactive flow...") + // Sigstore's default issuer and client ID + issuerURL := "https://oauth2.sigstore.dev/auth" + clientID := "sigstore" + // This will open a browser window for authentication + oidcToken, err := oauthflow.OIDConnect(issuerURL, clientID, "", "", oauthflow.DefaultIDTokenGetter) + if err != nil { + return nil, fmt.Errorf("failed to get interactive OIDC token: %w", err) + } + tok = oidcToken.RawString + identity = oidcToken.Subject + issuer = issuerURL + } + _ = tok + + // 2. Generate Ephemeral Key Pair + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ephemeral key: %w", err) + } + signer, err := sigstore_signature.LoadECDSASigner(privKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to load ephemeral signer: %w", err) + } + + // 3. Get Certificate from Fulcio using the real client + certBytes, err := c.getFulcioCertificate(ctx, privKey, identity, tok) + if err != nil { + return nil, fmt.Errorf("failed to get certificate from Fulcio: %w", err) + } + + // 4. Sign Data + sig, err := signer.SignMessage(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to sign data: %w", err) + } + + // 5. Upload to Rekor (Placeholder for real upload) + // rekorClient, _ := rekor.GetByProxy(rekorURL) + // entry, _ := cosign.TLogUpload(ctx, rekorClient, sig, certBytes, data) + + return &Signature{ + Signature: sig, + Certificate: certBytes, + Issuer: issuer, + Identity: identity, + Timestamp: time.Now().Unix(), + }, nil +} + +func (c *CosignAdapter) simulateKeyless(data []byte) (*Signature, error) { + return nil, fmt.Errorf("simulateKeyless is deprecated, use real keyless signing") +} + +func (c *CosignAdapter) signWithKey(data []byte) (*Signature, error) { + sig, err := c.signer.SignMessage(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to sign message: %w", err) + } + + certBytes, err := c.generateCertificate(c.privateKey, "local-key", "local") + if err != nil { + return nil, fmt.Errorf("failed to generate certificate: %w", err) + } + + sigObj := &Signature{ + Signature: sig, + Certificate: certBytes, + Issuer: "local", + Identity: "local-key", + Timestamp: time.Now().Unix(), + } + + return sigObj, nil +} + +func (c *CosignAdapter) getFulcioCertificate(ctx context.Context, privKey *ecdsa.PrivateKey, identity, oidcToken string) ([]byte, error) { + // Parse Fulcio URL + fulcioAddr, err := url.Parse(fulcioURL) + if err != nil { + return nil, fmt.Errorf("failed to parse Fulcio URL: %w", err) + } + + // Create Fulcio client + fulcioClient := api.NewClient(fulcioAddr) + + // Marshal public key to ASN.1 DER format + pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal public key: %w", err) + } + + // Create CertificateRequest with the public key + certReq := api.CertificateRequest{ + PublicKey: api.Key{ + Content: pubKeyBytes, + Algorithm: "ecdsa", + }, + } + + // We need to prove possession of the OIDC token's identity by signing the identity + // Fulcio expects a signature over the identity (e.g. email or subject) + proof, err := c.ecdsaSign(privKey, []byte(identity)) + if err != nil { + return nil, fmt.Errorf("failed to sign identity for proof: %w", err) + } + certReq.SignedEmailAddress = proof + + // Call Fulcio API to get certificate + certResp, err := fulcioClient.SigningCert(certReq, oidcToken) + if err != nil { + return nil, fmt.Errorf("Fulcio SigningCert failed: %w", err) + } + + return certResp.CertPEM, nil +} + +func (c *CosignAdapter) generateCertificate(privKey *ecdsa.PrivateKey, identity, issuer string) ([]byte, error) { + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: identity, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }) + + return certPEM, nil +} + +func (c *CosignAdapter) ecdsaSign(privKey *ecdsa.PrivateKey, data []byte) ([]byte, error) { + signer, err := sigstore_signature.LoadECDSASigner(privKey, crypto.SHA256) + if err != nil { + return nil, err + } + return signer.SignMessage(bytes.NewReader(data)) +} + +func (c *CosignAdapter) GetPrivateKeyPEM() ([]byte, error) { + if c.privateKey == nil { + return nil, fmt.Errorf("no private key available") + } + + derBytes, err := x509.MarshalECPrivateKey(c.privateKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key: %w", err) + } + + block := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: derBytes, + } + + return pem.EncodeToMemory(block), nil +} + +func (c *CosignAdapter) GetPublicKeyPEM() ([]byte, error) { + if c.privateKey == nil { + return nil, fmt.Errorf("no private key available") + } + + pubKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(&c.privateKey.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal public key: %w", err) + } + + return pubKeyBytes, nil +} + +func (c *CosignAdapter) VerifyData(data []byte, sig *Signature, allowUntrusted bool) error { + if sig == nil { + return fmt.Errorf("VerifyData: Signature value is nil") + } + if len(sig.Certificate) == 0 { + return fmt.Errorf("VerifyData: Signature.Certificate is empty") + } + + var verifier sigstore_signature.Verifier + var err error + + // If we have a certificate, it could be a keyless signature (Fulcio) or a key-based signature with a cert. + // For keyless, we should ideally verify the certificate chain and Rekor bundle. + // For now, we continue to support the simplified verification but using sigstore's abstractions. + + block, _ := pem.Decode(sig.Certificate) + if block != nil && block.Type == "CERTIFICATE" { + var cert *x509.Certificate + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + + if !allowUntrusted { + if cert.IsCA { + return fmt.Errorf("invalid certificate: must not be CA") + } + + // Build and verify the certificate chain + roots, err := fulcioroots.Get() + if err != nil { + return fmt.Errorf("failed to get Fulcio roots: %w", err) + } + opts := x509.VerifyOptions{ + Roots: roots, + KeyUsages: []x509.ExtKeyUsage{ + x509.ExtKeyUsageCodeSigning, + }, + CurrentTime: time.Unix(sig.Timestamp, 0), + } + if _, err := cert.Verify(opts); err != nil { + return fmt.Errorf("failed to verify certificate chain: %w", err) + } + + if time.Unix(sig.Timestamp, 0).Before(cert.NotBefore) || time.Unix(sig.Timestamp, 0).After(cert.NotAfter) { + return fmt.Errorf("certificate was not valid at signing time") + } + + // In a production environment, we would verify the certificate chain here + // against the Fulcio root set and system roots. + // roots, _ := fulcioroots.Get() + // cert.Verify(x509.VerifyOptions{Roots: roots}) + + // Check identity. Fulcio certs store identity in Subject Alternative Name (SAN) + // but many systems still look at CommonName or use specific extensions. + // Sigstore's verify library is usually used for this, but for now we'll check SANs. + foundIdentity := false + if cert.Subject.CommonName == sig.Identity { + foundIdentity = true + } else { + for _, email := range cert.EmailAddresses { + if email == sig.Identity { + foundIdentity = true + break + } + } + if !foundIdentity { + for _, uri := range cert.URIs { + if uri.String() == sig.Identity { + foundIdentity = true + break + } + } + } + } + + if sig.Identity != "" && !foundIdentity { + return fmt.Errorf("identity mismatch: certificate does not match signature identity %q (CN: %q, SANs: %v)", sig.Identity, cert.Subject.CommonName, cert.EmailAddresses) + } + + // Validate Rekor/CT evidence if Rekor bundle is present + if len(sig.RekorBundle) > 0 { + // In a full implementation, we would use cosign.VerifyBundle + // for now we acknowledge its presence for strict verification + } else if sig.Issuer != "local" && sig.Issuer != "" { + // For non-local certificates, we expect a Rekor bundle in strict mode + // But we'll allow it if we are in interactive mode (where Rekor might not be used) + if sig.Issuer != "https://oauth2.sigstore.dev/auth" { + return fmt.Errorf("strict verification failed: missing Rekor bundle for certificate from %q", sig.Issuer) + } + } + } + verifier, err = sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256) + if err != nil { + return fmt.Errorf("failed to load verifier from certificate: %w", err) + } + } else { + // If not a certificate, it must be a public key + if !allowUntrusted { + return fmt.Errorf("untrusted public key rejected: require valid x509 certificate chain") + } + + pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(sig.Certificate) + if err != nil { + // Try parsing as raw DER + pubKey, err = x509.ParsePKIXPublicKey(sig.Certificate) + if err != nil { + return fmt.Errorf("failed to unmarshal public key: %w", err) + } + } + + verifier, err = sigstore_signature.LoadVerifier(pubKey, crypto.SHA256) + if err != nil { + return fmt.Errorf("failed to load verifier: %w", err) + } + } + + if err := verifier.VerifySignature(bytes.NewReader(sig.Signature), bytes.NewReader(data)); err != nil { + return fmt.Errorf("invalid signature: %w", err) + } + + // In a full Cosign implementation, if we have a Rekor bundle, we would verify it here. + // sig.RekorBundle (if added to the Signature struct) could be used with cosign/pkg/cosign.VerifyBundle. + + if c.useKeyless && !allowUntrusted { + if sig.Issuer == "" || sig.Identity == "" { + return fmt.Errorf("keyless signature missing issuer or identity") + } + } + + return nil +} + +func (c *CosignAdapter) GetContentHash(obj interface{}) (string, error) { + data, err := json.Marshal(obj) + if err != nil { + return "", fmt.Errorf("failed to marshal object: %w", err) + } + + hash, err := utils.CanonicalHash(data) + if err != nil { + return "", err + } + + return hash, nil +} + +func (c *CosignAdapter) EncodeSignatureToAnnotations(sig *Signature) (map[string]string, error) { + annotations := make(map[string]string) + + annotations[AnnotationSignature] = base64.StdEncoding.EncodeToString(sig.Signature) + + if len(sig.Certificate) > 0 { + annotations[AnnotationCertificate] = base64.StdEncoding.EncodeToString(sig.Certificate) + } + if len(sig.RekorBundle) > 0 { + annotations[AnnotationRekorBundle] = base64.StdEncoding.EncodeToString(sig.RekorBundle) + } + if sig.Issuer != "" { + annotations[AnnotationIssuer] = sig.Issuer + } + if sig.Identity != "" { + annotations[AnnotationIdentity] = sig.Identity + } + annotations[AnnotationTimestamp] = fmt.Sprintf("%d", sig.Timestamp) + + return annotations, nil +} + +func (c *CosignAdapter) DecodeSignatureFromAnnotations(annotations map[string]string) (*Signature, error) { + sig := &Signature{} + + signatureB64, ok := annotations[AnnotationSignature] + if !ok { + return nil, fmt.Errorf("missing %s annotation", AnnotationSignature) + } + + var err error + sig.Signature, err = base64.StdEncoding.DecodeString(signatureB64) + if err != nil { + // Try raw if base64 fails + sig.Signature = []byte(signatureB64) + } + + if certB64, ok := annotations[AnnotationCertificate]; ok { + sig.Certificate, err = base64.StdEncoding.DecodeString(certB64) + if err != nil { + // Try raw if base64 fails + sig.Certificate = []byte(certB64) + } + } + + if rekorB64, ok := annotations[AnnotationRekorBundle]; ok { + sig.RekorBundle, err = base64.StdEncoding.DecodeString(rekorB64) + if err != nil { + // Try raw if base64 fails + sig.RekorBundle = []byte(rekorB64) + } + } + + sig.Issuer = annotations[AnnotationIssuer] + sig.Identity = annotations[AnnotationIdentity] + + if timestamp, ok := annotations[AnnotationTimestamp]; ok { + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse timestamp: %w", err) + } + sig.Timestamp = ts + } + + return sig, nil +} diff --git a/pkg/signature/cosign_adapter_test.go b/pkg/signature/cosign_adapter_test.go new file mode 100644 index 0000000000..b125f51753 --- /dev/null +++ b/pkg/signature/cosign_adapter_test.go @@ -0,0 +1,143 @@ +package signature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "testing" +) + +func TestNewCosignAdapterWithPrivateKey(t *testing.T) { + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + t.Run("Valid private key", func(t *testing.T) { + adapter, err := NewCosignAdapterWithPrivateKey(false, privKey) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if adapter.privateKey != privKey { + t.Error("Private key not set correctly") + } + }) + + t.Run("Nil private key", func(t *testing.T) { + _, err := NewCosignAdapterWithPrivateKey(false, nil) + if err == nil { + t.Error("Expected error for nil private key, got nil") + } + }) +} + +func TestCosignAdapter_GetKeysPEM(t *testing.T) { + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + adapter, _ := NewCosignAdapterWithPrivateKey(false, privKey) + + t.Run("GetPrivateKeyPEM", func(t *testing.T) { + pem, err := adapter.GetPrivateKeyPEM() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(pem) == 0 { + t.Error("Expected non-empty PEM") + } + }) + + t.Run("GetPublicKeyPEM", func(t *testing.T) { + pem, err := adapter.GetPublicKeyPEM() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(pem) == 0 { + t.Error("Expected non-empty PEM") + } + }) + + t.Run("No private key", func(t *testing.T) { + emptyAdapter := &CosignAdapter{} + _, err := emptyAdapter.GetPrivateKeyPEM() + if err == nil { + t.Error("Expected error, got nil") + } + _, err = emptyAdapter.GetPublicKeyPEM() + if err == nil { + t.Error("Expected error, got nil") + } + }) +} + +func TestWithPrivateKey(t *testing.T) { + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + opts := &SignOptions{} + WithPrivateKey(privKey)(opts) + if opts.PrivateKey != privKey { + t.Error("PrivateKey option not set correctly") + } +} + +func TestCosignSigner(t *testing.T) { + signer, err := NewCosignSigner(false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + data := []byte("test data") + sig, err := signer.Sign(data) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(sig.Signature) == 0 { + t.Error("Expected non-empty signature") + } +} + +func TestCosignAdapter_ecdsaSign(t *testing.T) { + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + adapter := &CosignAdapter{} + data := []byte("test data") + sig, err := adapter.ecdsaSign(privKey, data) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(sig) == 0 { + t.Error("Expected non-empty signature") + } +} + +func TestVerifyData_ErrorCases(t *testing.T) { + privKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + adapter, _ := NewCosignAdapterWithPrivateKey(false, privKey) + data := []byte("test data") + + t.Run("Invalid certificate PEM", func(t *testing.T) { + sig := &Signature{ + Signature: []byte("sig"), + Certificate: []byte("invalid-pem"), + } + err := adapter.VerifyData(data, sig, false) + if err == nil { + t.Error("Expected error for invalid certificate PEM, got nil") + } + }) + + t.Run("PublicKey is not ECDSA", func(t *testing.T) { + // Mock a non-ECDSA public key? Hard to do with current implementation. + // Skipping for now. + }) + + t.Run("Certificate is CA", func(t *testing.T) { + // Create a CA certificate + template := x509.Certificate{ + IsCA: true, + } + certDER, _ := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + sig := &Signature{ + Signature: []byte("sig"), + Certificate: certDER, + } + err := adapter.VerifyData(data, sig, false) + if err == nil { + t.Error("Expected error for CA certificate, got nil") + } + }) +} diff --git a/pkg/signature/interface.go b/pkg/signature/interface.go new file mode 100644 index 0000000000..720ca7a59c --- /dev/null +++ b/pkg/signature/interface.go @@ -0,0 +1,63 @@ +package signature + +import ( + "crypto/ecdsa" +) + +type Signer interface { + Sign(data []byte) (*Signature, error) +} + +type Verifier interface { + Verify(data []byte, sig *Signature) error +} + +type SignableObject interface { + GetAnnotations() map[string]string + SetAnnotations(annotations map[string]string) + GetUID() string + GetNamespace() string + GetName() string + GetContent() interface{} + GetUpdatedObject() interface{} +} + +type Signature struct { + Signature []byte + Certificate []byte + RekorBundle []byte + Issuer string + Identity string + Timestamp int64 +} + +type SignOptions struct { + UseKeyless bool + PrivateKey *ecdsa.PrivateKey +} + +type SignOption func(*SignOptions) + +func WithKeyless(useKeyless bool) SignOption { + return func(opts *SignOptions) { + opts.UseKeyless = useKeyless + } +} + +func WithPrivateKey(privateKey *ecdsa.PrivateKey) SignOption { + return func(opts *SignOptions) { + opts.PrivateKey = privateKey + } +} + +type VerifyOptions struct { + AllowUntrusted bool +} + +type VerifyOption func(*VerifyOptions) + +func WithUntrusted(allowUntrusted bool) VerifyOption { + return func(opts *VerifyOptions) { + opts.AllowUntrusted = allowUntrusted + } +} diff --git a/pkg/signature/profiles/adapter_test.go b/pkg/signature/profiles/adapter_test.go new file mode 100644 index 0000000000..0f9af91682 --- /dev/null +++ b/pkg/signature/profiles/adapter_test.go @@ -0,0 +1,335 @@ +package profiles + +import ( + "testing" + + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestApplicationProfileAdapter(t *testing.T) { + profile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "softwarecomposition.kubescape.io/v1beta1", + Kind: "ApplicationProfile", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ap", + Namespace: "default", + UID: types.UID("ap-uid-123"), + Labels: map[string]string{ + "app": "test", + }, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64"}, + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "nginx", + Capabilities: []string{"CAP_NET_BIND_SERVICE"}, + }, + }, + }, + } + + adapter := NewApplicationProfileAdapter(profile) + + if adapter == nil { + t.Fatal("Expected non-nil adapter") + } + + if adapter.GetUID() != "ap-uid-123" { + t.Errorf("Expected UID 'ap-uid-123', got '%s'", adapter.GetUID()) + } + + if adapter.GetNamespace() != "default" { + t.Errorf("Expected namespace 'default', got '%s'", adapter.GetNamespace()) + } + + if adapter.GetName() != "test-ap" { + t.Errorf("Expected name 'test-ap', got '%s'", adapter.GetName()) + } + + annotations := adapter.GetAnnotations() + if annotations == nil { + t.Error("Expected annotations map, got nil") + } + + testAnnotations := map[string]string{ + "test-key": "test-value", + } + adapter.SetAnnotations(testAnnotations) + if profile.Annotations["test-key"] != "test-value" { + t.Error("Failed to set annotations") + } + + content := adapter.GetContent() + if content == nil { + t.Fatal("Expected non-nil content") + } + + apContent, ok := content.(map[string]interface{}) + if !ok { + t.Fatal("Expected map[string]interface{} content type") + } + + metadata, ok := apContent["metadata"].(map[string]interface{}) + if !ok { + t.Fatal("Expected metadata to be map[string]interface{}") + } + + if metadata["name"] != "test-ap" { + t.Errorf("Expected content name 'test-ap', got '%v'", metadata["name"]) + } + + if metadata["namespace"] != "default" { + t.Errorf("Expected content namespace 'default', got '%v'", metadata["namespace"]) + } + + if apContent["apiVersion"] != "softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected apiVersion 'softwarecomposition.kubescape.io/v1beta1', got '%v'", apContent["apiVersion"]) + } + + if apContent["kind"] != "ApplicationProfile" { + t.Errorf("Expected kind 'ApplicationProfile', got '%v'", apContent["kind"]) + } +} + +func TestApplicationProfileAdapterSignAndVerify(t *testing.T) { + profile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "softwarecomposition.kubescape.io/v1beta1", + Kind: "ApplicationProfile", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "sign-test-ap", + Namespace: "default", + UID: types.UID("sign-ap-uid"), + Labels: map[string]string{ + "test": "signing", + }, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64", "arm64"}, + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "app", + Capabilities: []string{"CAP_NET_ADMIN"}, + }, + }, + }, + } + + adapter := NewApplicationProfileAdapter(profile) + + err := signature.SignObjectDisableKeyless(adapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + if profile.Annotations == nil { + t.Error("Expected annotations to be set on profile") + } + + if _, ok := profile.Annotations[signature.AnnotationSignature]; !ok { + t.Error("Expected signature annotation on profile") + } + + err = signature.VerifyObjectAllowUntrusted(adapter) + if err != nil { + t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err) + } +} + +func TestSeccompProfileAdapter(t *testing.T) { + profile := &v1beta1.SeccompProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "softwarecomposition.kubescape.io/v1beta1", + Kind: "SeccompProfile", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-seccomp", + Namespace: "default", + UID: types.UID("seccomp-uid-456"), + Labels: map[string]string{ + "seccomp": "test", + }, + }, + Spec: v1beta1.SeccompProfileSpec{ + Containers: []v1beta1.SingleSeccompProfile{ + { + Name: "test-container", + }, + }, + }, + } + + adapter := NewSeccompProfileAdapter(profile) + + if adapter == nil { + t.Fatal("Expected non-nil adapter") + } + + if adapter.GetUID() != "seccomp-uid-456" { + t.Errorf("Expected UID 'seccomp-uid-456', got '%s'", adapter.GetUID()) + } + + if adapter.GetNamespace() != "default" { + t.Errorf("Expected namespace 'default', got '%s'", adapter.GetNamespace()) + } + + if adapter.GetName() != "test-seccomp" { + t.Errorf("Expected name 'test-seccomp', got '%s'", adapter.GetName()) + } + + annotations := adapter.GetAnnotations() + if annotations == nil { + t.Error("Expected annotations map, got nil") + } + + testAnnotations := map[string]string{ + "seccomp-key": "seccomp-value", + } + adapter.SetAnnotations(testAnnotations) + if profile.Annotations["seccomp-key"] != "seccomp-value" { + t.Error("Failed to set annotations") + } + + content := adapter.GetContent() + if content == nil { + t.Fatal("Expected non-nil content") + } + + scContent, ok := content.(map[string]interface{}) + if !ok { + t.Fatal("Expected map[string]interface{} content type") + } + + metadata, ok := scContent["metadata"].(map[string]interface{}) + if !ok { + t.Fatal("Expected metadata to be map[string]interface{}") + } + + if metadata["name"] != "test-seccomp" { + t.Errorf("Expected content name 'test-seccomp', got '%v'", metadata["name"]) + } + + if metadata["namespace"] != "default" { + t.Errorf("Expected content namespace 'default', got '%v'", metadata["namespace"]) + } + + if scContent["apiVersion"] != "softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected apiVersion 'softwarecomposition.kubescape.io/v1beta1', got '%v'", scContent["apiVersion"]) + } + + if scContent["kind"] != "SeccompProfile" { + t.Errorf("Expected kind 'SeccompProfile', got '%v'", scContent["kind"]) + } +} + +func TestSeccompProfileAdapterSignAndVerify(t *testing.T) { + profile := &v1beta1.SeccompProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "softwarecomposition.kubescape.io/v1beta1", + Kind: "SeccompProfile", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "sign-test-seccomp", + Namespace: "default", + UID: types.UID("sign-seccomp-uid"), + Labels: map[string]string{ + "test": "seccomp-signing", + }, + }, + Spec: v1beta1.SeccompProfileSpec{ + Containers: []v1beta1.SingleSeccompProfile{ + { + Name: "app-container", + }, + }, + }, + } + + adapter := NewSeccompProfileAdapter(profile) + + err := signature.SignObjectDisableKeyless(adapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + if profile.Annotations == nil { + t.Error("Expected annotations to be set on profile") + } + + if _, ok := profile.Annotations[signature.AnnotationSignature]; !ok { + t.Error("Expected signature annotation on profile") + } + + err = signature.VerifyObjectAllowUntrusted(adapter) + if err != nil { + t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err) + } +} + +func TestAdapterUniqueness(t *testing.T) { + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unique-ap", + Namespace: "default", + UID: types.UID("ap-unique-uid"), + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64"}, + }, + } + + sp := &v1beta1.SeccompProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unique-sp", + Namespace: "default", + UID: types.UID("sp-unique-uid"), + }, + Spec: v1beta1.SeccompProfileSpec{}, + } + + apAdapter := NewApplicationProfileAdapter(ap) + spAdapter := NewSeccompProfileAdapter(sp) + + err := signature.SignObjectDisableKeyless(apAdapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed for ApplicationProfile: %v", err) + } + + err = signature.SignObjectDisableKeyless(spAdapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed for SeccompProfile: %v", err) + } + + apSig, err := signature.GetObjectSignature(apAdapter) + if err != nil { + t.Fatalf("GetObjectSignature failed for ApplicationProfile: %v", err) + } + + if apSig == nil { + t.Fatal("GetObjectSignature returned nil for ApplicationProfile") + } + + spSig, err := signature.GetObjectSignature(spAdapter) + if err != nil { + t.Fatalf("GetObjectSignature failed for SeccompProfile: %v", err) + } + + if spSig == nil { + t.Fatal("GetObjectSignature returned nil for SeccompProfile") + } + + if apSig.Issuer != "local" { + t.Errorf("Expected AP issuer 'local', got '%s'", apSig.Issuer) + } + + if spSig.Issuer != "local" { + t.Errorf("Expected SP issuer 'local', got '%s'", spSig.Issuer) + } +} diff --git a/pkg/signature/profiles/applicationprofile_adapter.go b/pkg/signature/profiles/applicationprofile_adapter.go new file mode 100644 index 0000000000..5a21b0a2e3 --- /dev/null +++ b/pkg/signature/profiles/applicationprofile_adapter.go @@ -0,0 +1,81 @@ +package profiles + +import ( + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" +) + +type ApplicationProfileAdapter struct { + profile *v1beta1.ApplicationProfile +} + +func NewApplicationProfileAdapter(profile *v1beta1.ApplicationProfile) *ApplicationProfileAdapter { + return &ApplicationProfileAdapter{ + profile: profile, + } +} + +func (a *ApplicationProfileAdapter) GetAnnotations() map[string]string { + if a.profile.Annotations == nil { + a.profile.Annotations = make(map[string]string) + } + return a.profile.Annotations +} + +func (a *ApplicationProfileAdapter) SetAnnotations(annotations map[string]string) { + a.profile.Annotations = annotations +} + +func (a *ApplicationProfileAdapter) GetUID() string { + return string(a.profile.UID) +} + +func (a *ApplicationProfileAdapter) GetNamespace() string { + return a.profile.Namespace +} + +func (a *ApplicationProfileAdapter) GetName() string { + return a.profile.Name +} + +func (a *ApplicationProfileAdapter) GetContent() interface{} { + // Normalize PolicyByRuleId to ensure consistent JSON representation + // Empty maps become {} instead of null + for i := range a.profile.Spec.Containers { + if a.profile.Spec.Containers[i].PolicyByRuleId == nil { + a.profile.Spec.Containers[i].PolicyByRuleId = make(map[string]v1beta1.RulePolicy) + } + } + for i := range a.profile.Spec.InitContainers { + if a.profile.Spec.InitContainers[i].PolicyByRuleId == nil { + a.profile.Spec.InitContainers[i].PolicyByRuleId = make(map[string]v1beta1.RulePolicy) + } + } + for i := range a.profile.Spec.EphemeralContainers { + if a.profile.Spec.EphemeralContainers[i].PolicyByRuleId == nil { + a.profile.Spec.EphemeralContainers[i].PolicyByRuleId = make(map[string]v1beta1.RulePolicy) + } + } + + apiVersion := a.profile.APIVersion + if apiVersion == "" { + apiVersion = "spdx.softwarecomposition.kubescape.io/v1beta1" + } + kind := a.profile.Kind + if kind == "" { + kind = "ApplicationProfile" + } + return map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": a.profile.Name, + "namespace": a.profile.Namespace, + "labels": a.profile.Labels, + }, + "spec": a.profile.Spec, + } +} + +func (a *ApplicationProfileAdapter) GetUpdatedObject() interface{} { + return a.profile +} diff --git a/pkg/signature/profiles/empty_typemeta_test.go b/pkg/signature/profiles/empty_typemeta_test.go new file mode 100644 index 0000000000..259ded5c77 --- /dev/null +++ b/pkg/signature/profiles/empty_typemeta_test.go @@ -0,0 +1,78 @@ +package profiles + +import ( + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestApplicationProfileAdapterEmptyTypeMeta(t *testing.T) { + profile := &v1beta1.ApplicationProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "", + Kind: "", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ap", + Namespace: "default", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64"}, + }, + } + + adapter := NewApplicationProfileAdapter(profile) + + content := adapter.GetContent() + if content == nil { + t.Fatal("Expected non-nil content") + } + + apContent, ok := content.(map[string]interface{}) + if !ok { + t.Fatal("Expected map[string]interface{} content type") + } + + if apContent["apiVersion"] != "spdx.softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected fallback apiVersion 'spdx.softwarecomposition.kubescape.io/v1beta1', got '%v'", apContent["apiVersion"]) + } + + if apContent["kind"] != "ApplicationProfile" { + t.Errorf("Expected fallback kind 'ApplicationProfile', got '%v'", apContent["kind"]) + } +} + +func TestSeccompProfileAdapterEmptyTypeMeta(t *testing.T) { + profile := &v1beta1.SeccompProfile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "", + Kind: "", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-seccomp", + Namespace: "default", + }, + Spec: v1beta1.SeccompProfileSpec{}, + } + + adapter := NewSeccompProfileAdapter(profile) + + content := adapter.GetContent() + if content == nil { + t.Fatal("Expected non-nil content") + } + + scContent, ok := content.(map[string]interface{}) + if !ok { + t.Fatal("Expected map[string]interface{} content type") + } + + if scContent["apiVersion"] != "spdx.softwarecomposition.kubescape.io/v1beta1" { + t.Errorf("Expected fallback apiVersion 'spdx.softwarecomposition.kubescape.io/v1beta1', got '%v'", scContent["apiVersion"]) + } + + if scContent["kind"] != "SeccompProfile" { + t.Errorf("Expected fallback kind 'SeccompProfile', got '%v'", scContent["kind"]) + } +} diff --git a/pkg/signature/profiles/networkneighborhood_adapter.go b/pkg/signature/profiles/networkneighborhood_adapter.go new file mode 100644 index 0000000000..e62caf4312 --- /dev/null +++ b/pkg/signature/profiles/networkneighborhood_adapter.go @@ -0,0 +1,63 @@ +package profiles + +import ( + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" +) + +type NetworkNeighborhoodAdapter struct { + nn *v1beta1.NetworkNeighborhood +} + +func NewNetworkNeighborhoodAdapter(nn *v1beta1.NetworkNeighborhood) *NetworkNeighborhoodAdapter { + return &NetworkNeighborhoodAdapter{ + nn: nn, + } +} + +func (a *NetworkNeighborhoodAdapter) GetAnnotations() map[string]string { + if a.nn.Annotations == nil { + a.nn.Annotations = make(map[string]string) + } + return a.nn.Annotations +} + +func (a *NetworkNeighborhoodAdapter) SetAnnotations(annotations map[string]string) { + a.nn.Annotations = annotations +} + +func (a *NetworkNeighborhoodAdapter) GetUID() string { + return string(a.nn.UID) +} + +func (a *NetworkNeighborhoodAdapter) GetNamespace() string { + return a.nn.Namespace +} + +func (a *NetworkNeighborhoodAdapter) GetName() string { + return a.nn.Name +} + +func (a *NetworkNeighborhoodAdapter) GetContent() interface{} { + apiVersion := a.nn.APIVersion + if apiVersion == "" { + apiVersion = "spdx.softwarecomposition.kubescape.io/v1beta1" + } + kind := a.nn.Kind + if kind == "" { + kind = "NetworkNeighborhood" + } + return map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": a.nn.Name, + "namespace": a.nn.Namespace, + "labels": a.nn.Labels, + }, + "spec": a.nn.Spec, + } +} + +func (a *NetworkNeighborhoodAdapter) GetUpdatedObject() interface{} { + return a.nn +} diff --git a/pkg/signature/profiles/networkneighborhood_adapter_test.go b/pkg/signature/profiles/networkneighborhood_adapter_test.go new file mode 100644 index 0000000000..7968784eb6 --- /dev/null +++ b/pkg/signature/profiles/networkneighborhood_adapter_test.go @@ -0,0 +1,99 @@ +package profiles + +import ( + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNetworkNeighborhoodAdapter(t *testing.T) { + nn := &v1beta1.NetworkNeighborhood{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkNeighborhood", + APIVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nn", + Namespace: "test-ns", + UID: "test-uid", + Annotations: map[string]string{ + "existing": "annotation", + }, + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "test-container", + Ingress: []v1beta1.NetworkNeighbor{ + { + Identifier: "test-neighbor", + }, + }, + }, + }, + }, + } + + adapter := NewNetworkNeighborhoodAdapter(nn) + + assert.Equal(t, "test-nn", adapter.GetName()) + assert.Equal(t, "test-ns", adapter.GetNamespace()) + assert.Equal(t, "test-uid", adapter.GetUID()) + + annotations := adapter.GetAnnotations() + assert.Equal(t, "annotation", annotations["existing"]) + + newAnnotations := map[string]string{"new": "annotation"} + adapter.SetAnnotations(newAnnotations) + assert.Equal(t, newAnnotations, nn.Annotations) + + content := adapter.GetContent().(map[string]interface{}) + assert.Equal(t, "NetworkNeighborhood", content["kind"]) + assert.Equal(t, "spdx.softwarecomposition.kubescape.io/v1beta1", content["apiVersion"]) + + metadata := content["metadata"].(map[string]interface{}) + assert.Equal(t, "test-nn", metadata["name"]) + assert.Equal(t, "test-ns", metadata["namespace"]) + + spec := content["spec"].(v1beta1.NetworkNeighborhoodSpec) + assert.Equal(t, 1, len(spec.Containers)) + assert.Equal(t, "test-container", spec.Containers[0].Name) + + assert.Equal(t, nn, adapter.GetUpdatedObject()) +} + +func TestNetworkNeighborhoodAdapter_EmptyTypeMeta(t *testing.T) { + nn := &v1beta1.NetworkNeighborhood{ + TypeMeta: metav1.TypeMeta{ + Kind: "", + APIVersion: "", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nn", + Namespace: "test-ns", + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "test-container", + }, + }, + }, + } + + adapter := NewNetworkNeighborhoodAdapter(nn) + content := adapter.GetContent().(map[string]interface{}) + + assert.Equal(t, "NetworkNeighborhood", content["kind"]) + assert.Equal(t, "spdx.softwarecomposition.kubescape.io/v1beta1", content["apiVersion"]) + + metadata := content["metadata"].(map[string]interface{}) + assert.Equal(t, "test-nn", metadata["name"]) + assert.Equal(t, "test-ns", metadata["namespace"]) + + spec := content["spec"].(v1beta1.NetworkNeighborhoodSpec) + assert.Equal(t, 1, len(spec.Containers)) + assert.Equal(t, "test-container", spec.Containers[0].Name) +} diff --git a/pkg/signature/profiles/rules_adapter.go b/pkg/signature/profiles/rules_adapter.go new file mode 100644 index 0000000000..248e3c1a91 --- /dev/null +++ b/pkg/signature/profiles/rules_adapter.go @@ -0,0 +1,60 @@ +package profiles + +import ( + rulemanagertypesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" +) + +type RulesAdapter struct { + rules *rulemanagertypesv1.Rules +} + +func NewRulesAdapter(rules *rulemanagertypesv1.Rules) *RulesAdapter { + return &RulesAdapter{ + rules: rules, + } +} + +func (r *RulesAdapter) GetAnnotations() map[string]string { + return r.rules.Annotations +} + +func (r *RulesAdapter) SetAnnotations(annotations map[string]string) { + r.rules.Annotations = annotations +} + +func (r *RulesAdapter) GetUID() string { + return string(r.rules.UID) +} + +func (r *RulesAdapter) GetNamespace() string { + return r.rules.Namespace +} + +func (r *RulesAdapter) GetName() string { + return r.rules.Name +} + +func (r *RulesAdapter) GetContent() interface{} { + apiVersion := r.rules.APIVersion + if apiVersion == "" { + apiVersion = "kubescape.io/v1" + } + kind := r.rules.Kind + if kind == "" { + kind = "Rules" + } + return map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": r.rules.Name, + "namespace": r.rules.Namespace, + "labels": r.rules.Labels, + }, + "spec": r.rules.Spec, + } +} + +func (r *RulesAdapter) GetUpdatedObject() interface{} { + return r.rules +} diff --git a/pkg/signature/profiles/rules_adapter_test.go b/pkg/signature/profiles/rules_adapter_test.go new file mode 100644 index 0000000000..f617e4ebe6 --- /dev/null +++ b/pkg/signature/profiles/rules_adapter_test.go @@ -0,0 +1,184 @@ +package profiles + +import ( + "strings" + "testing" + + rulemanagertypesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" + "github.com/kubescape/node-agent/pkg/signature" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" +) + +func TestRulesAdapterGetContent(t *testing.T) { + rules := &rulemanagertypesv1.Rules{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rules", + Namespace: "default", + UID: k8stypes.UID("test-uid"), + Labels: map[string]string{"label": "value"}, + }, + Spec: rulemanagertypesv1.RulesSpec{ + Rules: []rulemanagertypesv1.Rule{ + { + Enabled: true, + ID: "rule-1", + Name: "Test Rule", + Description: "A test rule", + Expressions: rulemanagertypesv1.RuleExpressions{ + Message: "message", + UniqueID: "uniqueId", + RuleExpression: []rulemanagertypesv1.RuleExpression{}, + }, + ProfileDependency: 0, + Severity: 1, + SupportPolicy: false, + Tags: []string{"test"}, + }, + }, + }, + } + + adapter := NewRulesAdapter(rules) + content := adapter.GetContent() + + if content == nil { + t.Fatal("Expected content not to be nil") + } + + contentMap, ok := content.(map[string]interface{}) + if !ok { + t.Fatal("Expected content to be a map") + } + + if contentMap["apiVersion"] != "kubescape.io/v1" { + t.Errorf("Expected apiVersion 'kubescape.io/v1', got '%v'", contentMap["apiVersion"]) + } + + if contentMap["kind"] != "Rules" { + t.Errorf("Expected kind 'Rules', got '%v'", contentMap["kind"]) + } + + metadata, ok := contentMap["metadata"].(map[string]interface{}) + if !ok { + t.Fatal("Expected metadata to be a map") + } + + if metadata["name"] != "test-rules" { + t.Errorf("Expected name 'test-rules', got '%v'", metadata["name"]) + } + + if metadata["namespace"] != "default" { + t.Errorf("Expected namespace 'default', got '%v'", metadata["namespace"]) + } + + if _, ok := contentMap["spec"]; !ok { + t.Error("Expected spec in content") + } +} + +func TestRulesAdapterSignAndVerify(t *testing.T) { + rules := &rulemanagertypesv1.Rules{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubescape.io/v1", + Kind: "Rules", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "sign-test-rules", + Namespace: "default", + UID: k8stypes.UID("sign-rules-uid"), + Labels: map[string]string{ + "test": "rules-signing", + }, + }, + Spec: rulemanagertypesv1.RulesSpec{ + Rules: []rulemanagertypesv1.Rule{ + { + Enabled: true, + ID: "test-rule-id", + Name: "Test Rule", + Description: "A test rule", + Expressions: rulemanagertypesv1.RuleExpressions{ + Message: "message", + UniqueID: "uniqueId", + RuleExpression: []rulemanagertypesv1.RuleExpression{}, + }, + ProfileDependency: 0, + Severity: 1, + SupportPolicy: false, + Tags: []string{"test"}, + }, + }, + }, + } + + adapter := NewRulesAdapter(rules) + + err := signature.SignObjectDisableKeyless(adapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + if rules.Annotations == nil { + t.Error("Expected annotations to be set on rules") + } + + if _, ok := rules.Annotations[signature.AnnotationSignature]; !ok { + t.Error("Expected signature annotation on rules") + } + + err = signature.VerifyObjectAllowUntrusted(adapter) + if err != nil { + t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err) + } +} + +func TestRulesAdapterSignAndVerifyWithTampering(t *testing.T) { + rules := &rulemanagertypesv1.Rules{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubescape.io/v1", + Kind: "Rules", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tamper-test-rules", + Namespace: "default", + }, + Spec: rulemanagertypesv1.RulesSpec{ + Rules: []rulemanagertypesv1.Rule{ + { + Enabled: true, + ID: "tamper-rule-id", + Name: "Tamper Test Rule", + Description: "A tamper test rule", + Expressions: rulemanagertypesv1.RuleExpressions{ + Message: "message", + UniqueID: "uniqueId", + RuleExpression: []rulemanagertypesv1.RuleExpression{}, + }, + ProfileDependency: 0, + Severity: 1, + SupportPolicy: false, + Tags: []string{"test"}, + }, + }, + }, + } + + adapter := NewRulesAdapter(rules) + + err := signature.SignObjectDisableKeyless(adapter) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + rules.Spec.Rules[0].Name = "Modified Rule Name" + + err = signature.VerifyObjectAllowUntrusted(adapter) + if err == nil { + t.Fatal("Expected verification to fail after tampering, but it succeeded") + } + + if !strings.Contains(err.Error(), "signature verification failed") { + t.Errorf("Expected signature verification error, got: %v", err) + } +} diff --git a/pkg/signature/profiles/seccompprofile_adapter.go b/pkg/signature/profiles/seccompprofile_adapter.go new file mode 100644 index 0000000000..8252cfbf76 --- /dev/null +++ b/pkg/signature/profiles/seccompprofile_adapter.go @@ -0,0 +1,63 @@ +package profiles + +import ( + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" +) + +type SeccompProfileAdapter struct { + profile *v1beta1.SeccompProfile +} + +func NewSeccompProfileAdapter(profile *v1beta1.SeccompProfile) *SeccompProfileAdapter { + return &SeccompProfileAdapter{ + profile: profile, + } +} + +func (s *SeccompProfileAdapter) GetAnnotations() map[string]string { + if s.profile.Annotations == nil { + s.profile.Annotations = make(map[string]string) + } + return s.profile.Annotations +} + +func (s *SeccompProfileAdapter) SetAnnotations(annotations map[string]string) { + s.profile.Annotations = annotations +} + +func (s *SeccompProfileAdapter) GetUID() string { + return string(s.profile.UID) +} + +func (s *SeccompProfileAdapter) GetNamespace() string { + return s.profile.Namespace +} + +func (s *SeccompProfileAdapter) GetName() string { + return s.profile.Name +} + +func (s *SeccompProfileAdapter) GetContent() interface{} { + apiVersion := s.profile.APIVersion + if apiVersion == "" { + apiVersion = "spdx.softwarecomposition.kubescape.io/v1beta1" + } + kind := s.profile.Kind + if kind == "" { + kind = "SeccompProfile" + } + return map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": s.profile.Name, + "namespace": s.profile.Namespace, + "labels": s.profile.Labels, + }, + "spec": s.profile.Spec, + } +} + +func (s *SeccompProfileAdapter) GetUpdatedObject() interface{} { + return s.profile +} diff --git a/pkg/signature/sign.go b/pkg/signature/sign.go new file mode 100644 index 0000000000..74ef6ba819 --- /dev/null +++ b/pkg/signature/sign.go @@ -0,0 +1,114 @@ +package signature + +import ( + "fmt" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +func SignObject(obj SignableObject, opts ...SignOption) error { + if obj == nil { + return fmt.Errorf("object is nil") + } + options := &SignOptions{ + UseKeyless: true, + } + + for _, opt := range opts { + opt(options) + } + + var adapter *CosignAdapter + var err error + + if options.PrivateKey != nil { + adapter, err = NewCosignAdapterWithPrivateKey(false, options.PrivateKey) + } else { + adapter, err = NewCosignAdapter(options.UseKeyless) + } + + if err != nil { + return fmt.Errorf("failed to create cosign adapter: %w", err) + } + + content := obj.GetContent() + + hash, err := adapter.GetContentHash(content) + if err != nil { + return fmt.Errorf("failed to compute content hash: %w", err) + } + + logger.L().Debug("Signing object", + helpers.String("namespace", obj.GetNamespace()), + helpers.String("name", obj.GetName()), + helpers.String("contentHash", hash)) + + sig, err := adapter.SignData([]byte(hash)) + if err != nil { + return fmt.Errorf("failed to sign object: %w", err) + } + + annotations, err := adapter.EncodeSignatureToAnnotations(sig) + if err != nil { + return fmt.Errorf("failed to encode signature to annotations: %w", err) + } + + existingAnnotations := obj.GetAnnotations() + if existingAnnotations == nil { + existingAnnotations = make(map[string]string) + } + + for k, v := range annotations { + existingAnnotations[k] = v + } + + obj.SetAnnotations(existingAnnotations) + + logger.L().Info("Successfully signed object", + helpers.String("namespace", obj.GetNamespace()), + helpers.String("name", obj.GetName()), + helpers.String("identity", sig.Identity), + helpers.String("issuer", sig.Issuer)) + + return nil +} + +func SignObjectDisableKeyless(obj SignableObject) error { + return SignObject(obj, WithKeyless(false)) +} + +func SignObjectKeyless(obj SignableObject) error { + return SignObject(obj, WithKeyless(true)) +} + +func GetObjectSignature(obj SignableObject) (*Signature, error) { + if obj == nil { + return nil, fmt.Errorf("GetObjectSignature: nil object") + } + annotations := obj.GetAnnotations() + if annotations == nil { + return nil, fmt.Errorf("object has no annotations") + } + + adapter := &CosignAdapter{} + sig, err := adapter.DecodeSignatureFromAnnotations(annotations) + if err != nil { + return nil, fmt.Errorf("failed to decode signature from annotations: %w", err) + } + + return sig, nil +} + +func IsSigned(obj SignableObject) bool { + if obj == nil { + return false + } + annotations := obj.GetAnnotations() + if annotations == nil { + return false + } + + _, ok := annotations[AnnotationSignature] + return ok +} diff --git a/pkg/signature/sign_test.go b/pkg/signature/sign_test.go new file mode 100644 index 0000000000..091484bce8 --- /dev/null +++ b/pkg/signature/sign_test.go @@ -0,0 +1,231 @@ +package signature + +import ( + "os" + "testing" +) + +type MockSignableObject struct { + annotations map[string]string + uid string + namespace string + name string + content interface{} +} + +func NewMockSignableObject(uid, namespace, name string, content interface{}) *MockSignableObject { + return &MockSignableObject{ + annotations: make(map[string]string), + uid: uid, + namespace: namespace, + name: name, + content: content, + } +} + +func (m *MockSignableObject) GetAnnotations() map[string]string { + return m.annotations +} + +func (m *MockSignableObject) SetAnnotations(annotations map[string]string) { + m.annotations = annotations +} + +func (m *MockSignableObject) GetUID() string { + return m.uid +} + +func (m *MockSignableObject) GetNamespace() string { + return m.namespace +} + +func (m *MockSignableObject) GetName() string { + return m.name +} + +func (m *MockSignableObject) GetContent() interface{} { + return m.content +} + +func (m *MockSignableObject) GetUpdatedObject() interface{} { + return m.content +} + +func TestSignObjectKeyless(t *testing.T) { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping TestSignObjectKeyless. Set ENABLE_KEYLESS_TESTS to run.") + } + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile", profileContent) + + err := SignObjectKeyless(profile) + if err != nil { + t.Fatalf("SignObjectKeyless failed: %v", err) + } + + if !IsSigned(profile) { + t.Error("Profile should be signed") + } + + sig, err := GetObjectSignature(profile) + if err != nil { + t.Fatalf("GetObjectSignature failed: %v", err) + } + + if len(sig.Signature) == 0 { + t.Error("Signature should not be empty") + } + + if len(sig.Certificate) == 0 { + t.Error("Certificate should not be empty") + } + + if sig.Issuer == "" { + t.Error("Issuer should not be empty for keyless signing") + } + + if sig.Identity == "" { + t.Error("Identity should not be empty for keyless signing") + } +} + +func TestSignObjectDisableKeyless(t *testing.T) { + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-key", profileContent) + + err := SignObjectDisableKeyless(profile) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + if !IsSigned(profile) { + t.Error("Profile should be signed") + } + + sig, err := GetObjectSignature(profile) + if err != nil { + t.Fatalf("GetObjectSignature failed: %v", err) + } + + if len(sig.Signature) == 0 { + t.Error("Signature should not be empty") + } + + if sig.Issuer != "local" { + t.Errorf("Expected issuer 'local', got '%s'", sig.Issuer) + } + + if sig.Identity != "local-key" { + t.Errorf("Expected identity 'local-key', got '%s'", sig.Identity) + } +} + +func TestIsSigned(t *testing.T) { + tests := []struct { + name string + profile *MockSignableObject + expected bool + }{ + { + name: "Unsigned profile", + profile: NewMockSignableObject("uid", "ns", "name", map[string]string{}), + expected: false, + }, + { + name: "Profile with empty annotations", + profile: &MockSignableObject{annotations: make(map[string]string)}, + expected: false, + }, + { + name: "Profile with signature annotation", + profile: func() *MockSignableObject { + p := NewMockSignableObject("uid", "ns", "name", map[string]string{}) + p.SetAnnotations(map[string]string{ + AnnotationSignature: "test-sig", + }) + return p + }(), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsSigned(tt.profile) + if result != tt.expected { + t.Errorf("IsSigned() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestGetObjectSignature(t *testing.T) { + tests := []struct { + name string + profile *MockSignableObject + wantErr bool + setupSign bool + setupAnnotations func(*MockSignableObject) + }{ + { + name: "Nil annotations", + profile: &MockSignableObject{uid: "uid", namespace: "ns", name: "name", content: map[string]string{}, annotations: nil}, + wantErr: true, + setupSign: false, + }, + { + name: "Missing signature annotation", + profile: NewMockSignableObject("uid", "ns", "name", map[string]string{}), + wantErr: true, + setupAnnotations: func(p *MockSignableObject) { + p.SetAnnotations(map[string]string{ + AnnotationIssuer: "test-issuer", + }) + }, + }, + { + name: "Complete signature", + profile: NewMockSignableObject("uid", "ns", "name", map[string]string{}), + wantErr: false, + setupSign: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupSign { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping subtest with SignObjectKeyless. Set ENABLE_KEYLESS_TESTS to run.") + } + SignObjectKeyless(tt.profile) + } else if tt.setupAnnotations != nil { + tt.setupAnnotations(tt.profile) + } + + sig, err := GetObjectSignature(tt.profile) + + if tt.wantErr { + if err == nil { + t.Error("Expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("GetObjectSignature failed: %v", err) + } + + if sig == nil { + t.Fatal("Expected signature, got nil") + } + }) + } +} diff --git a/pkg/signature/signer.go b/pkg/signature/signer.go new file mode 100644 index 0000000000..8f3197bd93 --- /dev/null +++ b/pkg/signature/signer.go @@ -0,0 +1,20 @@ +package signature + +type CosignSigner struct { + adapter *CosignAdapter +} + +func NewCosignSigner(useKeyless bool) (*CosignSigner, error) { + adapter, err := NewCosignAdapter(useKeyless) + if err != nil { + return nil, err + } + + return &CosignSigner{ + adapter: adapter, + }, nil +} + +func (s *CosignSigner) Sign(data []byte) (*Signature, error) { + return s.adapter.SignData(data) +} diff --git a/pkg/signature/verifier.go b/pkg/signature/verifier.go new file mode 100644 index 0000000000..4278757cb8 --- /dev/null +++ b/pkg/signature/verifier.go @@ -0,0 +1,38 @@ +package signature + +import "fmt" + +type CosignVerifier struct { + adapter *CosignAdapter +} + +func NewCosignVerifier(useKeyless bool) (*CosignVerifier, error) { + adapter, err := NewCosignAdapter(useKeyless) + if err != nil { + return nil, err + } + + return &CosignVerifier{ + adapter: adapter, + }, nil +} + +func (v *CosignVerifier) Verify(data []byte, sig *Signature) error { + if v == nil || v.adapter == nil { + return fmt.Errorf("verifier not initialized") + } + if sig == nil { + return fmt.Errorf("signature is nil") + } + return v.adapter.VerifyData(data, sig, false) +} + +func (v *CosignVerifier) VerifyAllowUntrusted(data []byte, sig *Signature) error { + if v == nil || v.adapter == nil { + return fmt.Errorf("verifier not initialized") + } + if sig == nil { + return fmt.Errorf("signature is nil") + } + return v.adapter.VerifyData(data, sig, true) +} diff --git a/pkg/signature/verify.go b/pkg/signature/verify.go new file mode 100644 index 0000000000..f5d3d9913a --- /dev/null +++ b/pkg/signature/verify.go @@ -0,0 +1,85 @@ +package signature + +import ( + "fmt" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +func VerifyObject(obj SignableObject, opts ...VerifyOption) error { + if obj == nil { + return fmt.Errorf("object is nil") + } + options := &VerifyOptions{ + AllowUntrusted: false, + } + + for _, opt := range opts { + opt(options) + } + + annotations := obj.GetAnnotations() + if annotations == nil { + return fmt.Errorf("%w (missing %s annotation)", ErrObjectNotSigned, AnnotationSignature) + } + + if _, ok := annotations[AnnotationSignature]; !ok { + return fmt.Errorf("%w (missing %s annotation)", ErrObjectNotSigned, AnnotationSignature) + } + + // useKeyless=true is fine for verification since we use the certificate + // stored in the object annotations, regardless of how the object was signed + adapter, err := NewCosignAdapter(true) + if err != nil { + return fmt.Errorf("failed to create cosign adapter: %w", err) + } + + sig, err := adapter.DecodeSignatureFromAnnotations(annotations) + if err != nil { + return fmt.Errorf("failed to decode signature from annotations: %w", err) + } + + content := obj.GetContent() + hash, err := adapter.GetContentHash(content) + if err != nil { + return fmt.Errorf("failed to compute content hash: %w", err) + } + + verifier, err := NewCosignVerifier(true) + if err != nil { + return fmt.Errorf("failed to create verifier: %w", err) + } + + var verifyErr error + if options.AllowUntrusted { + verifyErr = verifier.VerifyAllowUntrusted([]byte(hash), sig) + } else { + verifyErr = verifier.Verify([]byte(hash), sig) + } + + if verifyErr != nil { + logger.L().Warning("Object signature verification failed", + helpers.String("namespace", obj.GetNamespace()), + helpers.String("name", obj.GetName()), + helpers.String("error", verifyErr.Error())) + + return fmt.Errorf("signature verification failed: %w", verifyErr) + } + + logger.L().Info("Successfully verified object signature", + helpers.String("namespace", obj.GetNamespace()), + helpers.String("name", obj.GetName()), + helpers.String("identity", sig.Identity), + helpers.String("issuer", sig.Issuer)) + + return nil +} + +func VerifyObjectStrict(obj SignableObject) error { + return VerifyObject(obj, WithUntrusted(false)) +} + +func VerifyObjectAllowUntrusted(obj SignableObject) error { + return VerifyObject(obj, WithUntrusted(true)) +} diff --git a/pkg/signature/verify_test.go b/pkg/signature/verify_test.go new file mode 100644 index 0000000000..70973bf2ee --- /dev/null +++ b/pkg/signature/verify_test.go @@ -0,0 +1,435 @@ +package signature + +import ( + "io" + "os" + "strings" + "testing" + + logger "github.com/kubescape/go-logger" + "github.com/kubescape/node-agent/pkg/signature/profiles" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestVerifyObjectStrict(t *testing.T) { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping TestVerifyObjectStrict. Set ENABLE_KEYLESS_TESTS to run.") + } + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + "value": 123, + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-verify", profileContent) + + err := SignObjectKeyless(profile) + if err != nil { + t.Fatalf("SignObjectKeyless failed: %v", err) + } + + err = VerifyObjectStrict(profile) + if err != nil { + t.Fatalf("VerifyObjectStrict failed: %v", err) + } +} + +func TestVerifyObjectAllowUntrusted(t *testing.T) { + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + "value": 456, + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-verify-2", profileContent) + + err := SignObjectDisableKeyless(profile) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed: %v", err) + } + + err = VerifyObjectAllowUntrusted(profile) + if err != nil { + t.Fatalf("VerifyObjectAllowUntrusted failed: %v", err) + } +} + +func TestVerifyObjectTampered(t *testing.T) { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping TestVerifyObjectTampered. Set ENABLE_KEYLESS_TESTS to run.") + } + originalContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + "value": 789, + "confident": "secret", + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-tamper", originalContent) + + err := SignObjectKeyless(profile) + if err != nil { + t.Fatalf("SignObjectKeyless failed: %v", err) + } + + tamperedContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + "value": 999, + "confident": "mod", + } + profile.content = tamperedContent + + err = VerifyObjectStrict(profile) + if err == nil { + t.Error("Expected verification failure for tampered profile, got success") + } +} + +func TestVerifyObjectNoAnnotations(t *testing.T) { + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-no-sig", profileContent) + + err := VerifyObjectStrict(profile) + if err == nil { + t.Error("Expected error for profile without annotations, got nil") + } +} + +func TestVerifyObjectMissingSignature(t *testing.T) { + profileContent := map[string]interface{}{ + "type": "test-profile", + "data": "test-data", + } + + profile := NewMockSignableObject("test-uid", "test-ns", "test-profile-missing-sig", profileContent) + profile.SetAnnotations(map[string]string{ + AnnotationIssuer: "test-issuer", + AnnotationIdentity: "test-identity", + }) + + err := VerifyObjectStrict(profile) + if err == nil { + t.Error("Expected error for profile without signature annotation, got nil") + } +} + +func TestSignAndVerifyRoundTrip(t *testing.T) { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping TestSignAndVerifyRoundTrip. Set ENABLE_KEYLESS_TESTS to run.") + } + profileContent := map[string]interface{}{ + "type": "roundtrip-profile", + "containers": []string{"nginx", "redis"}, + "capabilities": []string{"NET_BIND_SERVICE"}, + "networkPolicy": "allow", + } + + profile := NewMockSignableObject("roundtrip-uid", "roundtrip-ns", "roundtrip-profile", profileContent) + + err := SignObjectKeyless(profile) + if err != nil { + t.Fatalf("SignObjectKeyless failed: %v", err) + } + + if !IsSigned(profile) { + t.Fatal("Profile should be signed after signing") + } + + sig, err := GetObjectSignature(profile) + if err != nil { + t.Fatalf("GetObjectSignature failed: %v", err) + } + + if len(sig.Signature) == 0 { + t.Error("Signature should not be empty") + } + + err = VerifyObjectStrict(profile) + if err != nil { + t.Fatalf("VerifyObjectStrict failed after signing: %v", err) + } +} + +func TestSignAndVerifyDifferentKeys(t *testing.T) { + if os.Getenv("ENABLE_KEYLESS_TESTS") == "" { + t.Skip("Skipping TestSignAndVerifyDifferentKeys. Set ENABLE_KEYLESS_TESTS to run.") + } + profileContent := map[string]interface{}{ + "type": "multi-key-test", + "data": "data", + } + + profile1 := NewMockSignableObject("uid1", "ns", "profile1", profileContent) + profile2 := NewMockSignableObject("uid2", "ns", "profile2", profileContent) + + err := SignObjectDisableKeyless(profile1) + if err != nil { + t.Fatalf("SignObjectDisableKeyless failed for profile1: %v", err) + } + + err = SignObjectKeyless(profile2) + if err != nil { + t.Fatalf("SignObjectKeyless failed for profile2: %v", err) + } + + sig1, err := GetObjectSignature(profile1) + if err != nil { + t.Fatalf("GetObjectSignature failed for profile1: %v", err) + } + + sig2, err := GetObjectSignature(profile2) + if err != nil { + t.Fatalf("GetObjectSignature failed for profile2: %v", err) + } + + if sig1.Issuer != "local" { + t.Errorf("Expected key-based signing issuer 'local', got '%s'", sig1.Issuer) + } + + if sig1.Identity != "local-key" { + t.Errorf("Expected key-based signing identity 'local-key', got '%s'", sig1.Identity) + } + + if sig2.Issuer == "" { + t.Errorf("Expected keyless signing to have issuer, got empty") + } + + if sig2.Identity == "" { + t.Errorf("Expected keyless signing to have identity, got empty") + } +} + +// captureLogOutput redirects the global logger to a pipe, runs fn, and returns +// the captured log text. The logger is restored to its previous writer afterward. +func captureLogOutput(t *testing.T, fn func()) string { + t.Helper() + + // Ensure the global logger is initialized as pretty (supports SetWriter). + logger.InitLogger("pretty") + + oldWriter := logger.L().GetWriter() + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe: %v", err) + } + logger.L().SetWriter(w) + + fn() + + w.Close() + var buf strings.Builder + io.Copy(&buf, r) + r.Close() + + // Restore original writer. + logger.L().SetWriter(oldWriter) + + return buf.String() +} + +// TestTamperedAPLogsWarning signs an ApplicationProfile, tampers with it, +// verifies it, and asserts the warning log contains the expected fields: +// namespace, name, and "Object signature verification failed". +func TestTamperedAPLogsWarning(t *testing.T) { + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tamper-warn-ap", + Namespace: "tamper-ns", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + Execs: []v1beta1.ExecCalls{{Path: "/usr/bin/curl"}}, + Syscalls: []string{"read", "write"}, + }, + }, + }, + } + + adapter := profiles.NewApplicationProfileAdapter(ap) + if err := SignObjectDisableKeyless(adapter); err != nil { + t.Fatalf("sign failed: %v", err) + } + + // Tamper: add an exec entry. + ap.Spec.Containers[0].Execs = append(ap.Spec.Containers[0].Execs, + v1beta1.ExecCalls{Path: "/usr/bin/nslookup"}) + + tamperedAdapter := profiles.NewApplicationProfileAdapter(ap) + + logOutput := captureLogOutput(t, func() { + err := VerifyObjectAllowUntrusted(tamperedAdapter) + if err == nil { + t.Error("expected verification to fail for tampered AP") + } + }) + + // Assert warning log contains expected fields. + if !strings.Contains(logOutput, "Object signature verification failed") { + t.Errorf("expected warning message in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "tamper-ns") { + t.Errorf("expected namespace 'tamper-ns' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "tamper-warn-ap") { + t.Errorf("expected name 'tamper-warn-ap' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "invalid signature") { + t.Errorf("expected 'invalid signature' in log output, got:\n%s", logOutput) + } +} + +// TestTamperedNNLogsWarning signs a NetworkNeighborhood, tampers with it, +// verifies it, and asserts the warning log contains the expected fields. +func TestTamperedNNLogsWarning(t *testing.T) { + nn := &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tamper-warn-nn", + Namespace: "tamper-ns", + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "curl", + Egress: []v1beta1.NetworkNeighbor{ + { + Identifier: "legit", + DNSNames: []string{"example.com."}, + IPAddress: "93.184.216.34", + }, + }, + }, + }, + }, + } + + adapter := profiles.NewNetworkNeighborhoodAdapter(nn) + if err := SignObjectDisableKeyless(adapter); err != nil { + t.Fatalf("sign failed: %v", err) + } + + // Tamper: add an egress entry. + nn.Spec.Containers[0].Egress = append(nn.Spec.Containers[0].Egress, + v1beta1.NetworkNeighbor{ + Identifier: "evil", + DNSNames: []string{"evil-c2.io."}, + IPAddress: "6.6.6.6", + }) + + tamperedAdapter := profiles.NewNetworkNeighborhoodAdapter(nn) + + logOutput := captureLogOutput(t, func() { + err := VerifyObjectAllowUntrusted(tamperedAdapter) + if err == nil { + t.Error("expected verification to fail for tampered NN") + } + }) + + if !strings.Contains(logOutput, "Object signature verification failed") { + t.Errorf("expected warning message in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "tamper-ns") { + t.Errorf("expected namespace 'tamper-ns' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "tamper-warn-nn") { + t.Errorf("expected name 'tamper-warn-nn' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "invalid signature") { + t.Errorf("expected 'invalid signature' in log output, got:\n%s", logOutput) + } +} + +// TestSuccessfulVerifyLogsInfo verifies that a valid signature produces the +// "Successfully verified object signature" info log with identity and issuer. +func TestSuccessfulVerifyLogsInfo(t *testing.T) { + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid-ap", + Namespace: "valid-ns", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "nginx", + Execs: []v1beta1.ExecCalls{{Path: "/usr/sbin/nginx"}}, + Syscalls: []string{"read", "write", "openat"}, + }, + }, + }, + } + + adapter := profiles.NewApplicationProfileAdapter(ap) + if err := SignObjectDisableKeyless(adapter); err != nil { + t.Fatalf("sign failed: %v", err) + } + + logOutput := captureLogOutput(t, func() { + if err := VerifyObjectAllowUntrusted(adapter); err != nil { + t.Fatalf("expected verification to succeed: %v", err) + } + }) + + if !strings.Contains(logOutput, "Successfully verified object signature") { + t.Errorf("expected info message in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "valid-ns") { + t.Errorf("expected namespace 'valid-ns' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "valid-ap") { + t.Errorf("expected name 'valid-ap' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "local-key") { + t.Errorf("expected identity 'local-key' in log output, got:\n%s", logOutput) + } +} + +// TestSignLogsInfo verifies that signing an object produces the +// "Successfully signed object" info log with identity and issuer. +func TestSignLogsInfo(t *testing.T) { + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sign-log-ap", + Namespace: "sign-ns", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "app", + Execs: []v1beta1.ExecCalls{{Path: "/app/main"}}, + Syscalls: []string{"read"}, + }, + }, + }, + } + + adapter := profiles.NewApplicationProfileAdapter(ap) + + logOutput := captureLogOutput(t, func() { + if err := SignObjectDisableKeyless(adapter); err != nil { + t.Fatalf("sign failed: %v", err) + } + }) + + if !strings.Contains(logOutput, "Successfully signed object") { + t.Errorf("expected sign info message in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "sign-ns") { + t.Errorf("expected namespace 'sign-ns' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "sign-log-ap") { + t.Errorf("expected name 'sign-log-ap' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "local-key") { + t.Errorf("expected identity 'local-key' in log output, got:\n%s", logOutput) + } + if !strings.Contains(logOutput, "local") { + t.Errorf("expected issuer 'local' in log output, got:\n%s", logOutput) + } +} diff --git a/scripts/HOWTO.md b/scripts/HOWTO.md new file mode 100644 index 0000000000..5be44df97c --- /dev/null +++ b/scripts/HOWTO.md @@ -0,0 +1,104 @@ +# Fork Workflow: k8sstormcenter/node-agent + +## Branch structure + +``` +upstream/main: A --- B --- C --- D (kubescape/node-agent) + \ +fork main: D --- [feat-X] --- [fork-only: .github/*] + \ +feature branch: feat/my-feature (1-2 clean commits) +``` + +**Rules:** +- Fork `main` always has a **fork-only `.github/` commit as the tip** — this is never sent upstream. +- Feature branches start from `main~1` (before the fork-only commit). +- Each feature is a small, focused branch with clean commits. + +## Day-to-day workflow + +### 1. Sync fork with upstream + +```bash +git fetch upstream +git checkout main + +# Rebase your features onto latest upstream (fork-only commit stays on top) +git rebase upstream/main + +# Force-push (safe — your main is the source of truth) +git push origin main --force-with-lease +``` + +### 2. Start a new feature + +```bash +# Always branch from main~1 (before fork-only commit) +git checkout -b feat/my-feature main~1 + +# Develop... +# Commit (sign your commits) +# Test locally with local-ci.sh or CI +``` + +### 3. Test on your fork + +```bash +# Push feature branch to your fork +git push origin feat/my-feature + +# Merge into fork main (keeps fork-only commit on top): +git checkout main +git rebase --onto feat/my-feature main~2 main +# This replays [feat/my-feature commits] + [fork-only commit] onto the feature +git push origin main --force-with-lease +``` + +Or simpler: just push the feature branch and trigger CI via workflow_dispatch. + +### 4. Create upstream PR + +```bash +# Use the script — it cherry-picks your feature onto upstream/main, +# stripping any .github/ changes automatically +./scripts/upstream-pr.sh feat/my-feature + +# Verify it's clean +git diff --stat upstream/main upstream/my-feature -- .github/ # should be empty + +# Push to upstream and open PR +git push upstream upstream/my-feature +``` + +Then open the PR at `https://github.com/kubescape/node-agent/compare/main...upstream/my-feature` + +### 5. After upstream merges your PR + +```bash +# Sync +git fetch upstream +git checkout main +git rebase upstream/main +git push origin main --force-with-lease + +# Clean up +git branch -d feat/my-feature +git branch -d upstream/my-feature +git push origin --delete feat/my-feature +``` + +## What NOT to do + +- **Don't develop on `main` directly** — always use feature branches. +- **Don't squash-merge upstream into your fork** — this is what caused the old mess (regressions baked into squash commits). Use `rebase` instead. +- **Don't mix `.github/` changes with feature commits** — keep them in the fork-only tip commit only. +- **Don't push the node-agent image as `latest` from feature branches** — use dedicated tags (`build.yaml` is already configured for this). + +## Key files + +| File | Purpose | +|---|---| +| `scripts/upstream-pr.sh` | Creates upstream-clean branches for PRs | +| `tests/scripts/local-ci.sh` | Runs component tests locally in Kind | +| `.github/workflows/component-tests.yaml` | Fork CI (triggers on `main`) | +| `.github/workflows/build.yaml` | Builds node-agent image (no `latest` tag) | diff --git a/scripts/upstream-pr.sh b/scripts/upstream-pr.sh new file mode 100755 index 0000000000..773f38ed69 --- /dev/null +++ b/scripts/upstream-pr.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# upstream-pr.sh — Create an upstream-clean branch for PRing to kubescape/node-agent +# +# Your fork's main looks like: +# +# upstream/main --- [feat-A] --- [feat-B] --- [fork-only: .github/*] +# ^^ always the tip +# +# This script takes a feature branch (based on main~1), cherry-picks its +# commits onto upstream/main, and strips any .github/ changes so the +# result is clean for an upstream PR. +# +# Usage: +# ./scripts/upstream-pr.sh [upstream-branch-name] +# +# Examples: +# ./scripts/upstream-pr.sh feat/signature-verification +# ./scripts/upstream-pr.sh feat/signature-verification upstream-sig-verify +# +set -euo pipefail + +FEATURE="${1:?Usage: $0 [upstream-branch-name]}" +# Default upstream branch name: strip "feat/" prefix, prepend "upstream/" +DEFAULT_NAME="upstream/${FEATURE#feat/}" +UPSTREAM_BRANCH="${2:-$DEFAULT_NAME}" + +echo "=== upstream-pr ===" +echo " Feature branch : $FEATURE" +echo " Upstream branch: $UPSTREAM_BRANCH" +echo "" + +# Ensure we have the latest upstream +git fetch upstream + +# Fail if the branch already exists +if git rev-parse --verify "$UPSTREAM_BRANCH" &>/dev/null; then + echo "ERROR: Branch '$UPSTREAM_BRANCH' already exists." + echo " Delete it first: git branch -D $UPSTREAM_BRANCH" + exit 1 +fi + +# Find commits on the feature branch that are above origin/main +COMMITS=$(git rev-list --reverse origin/main.."$FEATURE") +if [ -z "$COMMITS" ]; then + echo "ERROR: No commits found on '$FEATURE' above origin/main." + exit 1 +fi + +# Create branch from upstream/main +git checkout -b "$UPSTREAM_BRANCH" upstream/main + +APPLIED=0 +SKIPPED=0 +for commit in $COMMITS; do + SUBJECT=$(git log --oneline -1 "$commit") + + # Skip commits that ONLY touch .github/ + NON_GITHUB=$(git diff-tree --no-commit-id --name-only -r "$commit" | grep -v '^\.github/' || true) + if [ -z "$NON_GITHUB" ]; then + echo " SKIP (github-only): $SUBJECT" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " APPLY: $SUBJECT" + git cherry-pick "$commit" --no-commit + + # Remove any .github changes that came along for the ride + if git diff --cached --name-only | grep -q '^\.github/'; then + git reset HEAD -- .github/ &>/dev/null || true + git checkout -- .github/ &>/dev/null || true + fi + + # Re-commit with the original message and author + git commit -C "$commit" + APPLIED=$((APPLIED + 1)) +done + +echo "" +echo "=== Done ===" +echo " Applied: $APPLIED commits" +echo " Skipped: $SKIPPED commits (.github-only)" +echo "" +echo "Verify:" +echo " git log --oneline $UPSTREAM_BRANCH --not upstream/main" +echo " git diff --stat upstream/main $UPSTREAM_BRANCH -- .github/ # should be empty" +echo "" +echo "Push to upstream:" +echo " git push upstream $UPSTREAM_BRANCH" +echo "" +echo "Then open PR at: https://github.com/kubescape/node-agent/compare/main...$UPSTREAM_BRANCH" diff --git a/tests/chart/crds/rules.crd.yaml b/tests/chart/crds/rules.crd.yaml index e4e1155eaf..f8cc94ee42 100644 --- a/tests/chart/crds/rules.crd.yaml +++ b/tests/chart/crds/rules.crd.yaml @@ -91,6 +91,19 @@ spec: type: object additionalProperties: true description: "State information for the rule" + agentVersionRequirement: + type: string + description: "Agent version requirement to evaluate this rule (supports semver ranges like ~1.0, >=1.2.0, etc.)" + isTriggerAlert: + type: boolean + description: "Whether the rule is a trigger alert" + default: true + mitreTechnique: + type: string + description: "MITRE technique associated with the rule" + mitreTactic: + type: string + description: "MITRE tactic associated with the rule" required: - enabled - id @@ -100,7 +113,9 @@ spec: - profileDependency - severity - supportPolicy - - tags + - isTriggerAlert + - mitreTechnique + - mitreTactic required: - rules subresources: diff --git a/tests/chart/crds/runtime-rule-binding.crd.yaml b/tests/chart/crds/runtime-rule-binding.crd.yaml index d37280065e..d01b29b443 100644 --- a/tests/chart/crds/runtime-rule-binding.crd.yaml +++ b/tests/chart/crds/runtime-rule-binding.crd.yaml @@ -95,4 +95,4 @@ spec: items: type: string severity: - type: string \ No newline at end of file + type: string diff --git a/tests/chart/templates/node-agent/default-rules.yaml b/tests/chart/templates/node-agent/default-rules.yaml index 55fd1b527e..e24ec84b5b 100644 --- a/tests/chart/templates/node-agent/default-rules.yaml +++ b/tests/chart/templates/node-agent/default-rules.yaml @@ -1,7 +1,7 @@ apiVersion: kubescape.io/v1 kind: Rules metadata: - name: kubescape-rules + name: default-rules namespace: kubescape annotations: kubescape.io/namespace: kubescape @@ -30,6 +30,7 @@ spec: - "process" - "exec" - "applicationprofile" + - "context:kubernetes" - name: "Files Access Anomalies in container" enabled: true id: "R0002" @@ -69,6 +70,7 @@ spec: - "file" - "open" - "applicationprofile" + - "context:kubernetes" - name: "Syscalls Anomalies in container" enabled: true id: "R0003" @@ -89,6 +91,7 @@ spec: - "anomaly" - "syscall" - "applicationprofile" + - "context:kubernetes" - name: "Linux Capabilities Anomalies in container" enabled: true id: "R0004" @@ -109,6 +112,7 @@ spec: - "anomaly" - "capabilities" - "applicationprofile" + - "context:kubernetes" - name: "DNS Anomalies in container" enabled: true id: "R0005" @@ -118,17 +122,18 @@ spec: uniqueId: "event.comm + '_' + event.name" ruleExpression: - eventType: "dns" - expression: "!event.name.endsWith('.svc.cluster.local.') && !nn.is_domain_in_egress(event.containerId, event.name)" + expression: "!nn.is_domain_in_egress(event.containerId, event.name)" profileDependency: 0 severity: 1 supportPolicy: false - isTriggerAlert: false + isTriggerAlert: true mitreTactic: "TA0011" mitreTechnique: "T1071.004" tags: - "dns" - "anomaly" - "networkprofile" + - "context:kubernetes" - name: "Unexpected service account token access" enabled: true id: "R0006" @@ -139,15 +144,12 @@ spec: ruleExpression: - eventType: "open" expression: > - ((event.path.startsWith('/run/secrets/kubernetes.io/serviceaccount') && event.path.endsWith('/token')) || + ((event.path.startsWith('/run/secrets/kubernetes.io/serviceaccount') && event.path.endsWith('/token')) || (event.path.startsWith('/var/run/secrets/kubernetes.io/serviceaccount') && event.path.endsWith('/token')) || (event.path.startsWith('/run/secrets/eks.amazonaws.com/serviceaccount') && event.path.endsWith('/token')) || (event.path.startsWith('/var/run/secrets/eks.amazonaws.com/serviceaccount') && event.path.endsWith('/token'))) && - !ap.was_path_opened_with_prefix(event.containerId, '/run/secrets/kubernetes.io/serviceaccount') && - !ap.was_path_opened_with_prefix(event.containerId, '/var/run/secrets/kubernetes.io/serviceaccount') && - !ap.was_path_opened_with_prefix(event.containerId, '/run/secrets/eks.amazonaws.com/serviceaccount') && - !ap.was_path_opened_with_prefix(event.containerId, '/var/run/secrets/eks.amazonaws.com/serviceaccount') - profileDependency: 1 + !ap.was_path_opened_with_suffix(event.containerId, '/token') + profileDependency: 0 severity: 5 supportPolicy: false isTriggerAlert: true @@ -157,6 +159,7 @@ spec: - "anomaly" - "serviceaccount" - "applicationprofile" + - "context:kubernetes" - name: "Workload uses Kubernetes API unexpectedly" enabled: true id: "R0007" @@ -172,7 +175,7 @@ spec: profileDependency: 0 severity: 5 # Medium supportPolicy: false - isTriggerAlert: true + isTriggerAlert: false mitreTactic: "TA0008" mitreTechnique: "T1210" tags: @@ -180,6 +183,7 @@ spec: - "network" - "anomaly" - "applicationprofile" + - "context:kubernetes" - name: "Read Environment Variables from procfs" enabled: true id: "R0008" @@ -190,7 +194,7 @@ spec: ruleExpression: - eventType: "open" expression: > - event.path.startsWith('/proc/') && + event.path.startsWith('/proc/') && event.path.endsWith('/environ') && !ap.was_path_opened_with_suffix(event.containerId, '/environ') profileDependency: 0 # Required @@ -204,6 +208,7 @@ spec: - "procfs" - "environment" - "applicationprofile" + - "context:kubernetes" - name: "eBPF Program Load" enabled: true id: "R0009" @@ -224,6 +229,7 @@ spec: - "bpf" - "ebpf" - "applicationprofile" + - "context:kubernetes" - name: "Unexpected Sensitive File Access" enabled: true id: "R0010" @@ -244,8 +250,9 @@ spec: - "files" - "anomaly" - "applicationprofile" + - "context:kubernetes" - name: "Unexpected Egress Network Traffic" - enabled: false + enabled: true id: "R0011" description: "Detecting unexpected egress network traffic that is not whitelisted by application profile." expressions: @@ -257,7 +264,7 @@ spec: profileDependency: 0 severity: 5 # Medium supportPolicy: false - isTriggerAlert: false + isTriggerAlert: true mitreTactic: "TA0010" mitreTechnique: "T1041" tags: @@ -265,6 +272,7 @@ spec: - "network" - "anomaly" - "networkprofile" + - "context:kubernetes" - name: "Process executed from malicious source" enabled: true id: "R1000" @@ -276,7 +284,7 @@ spec: - eventType: "exec" expression: > (event.exepath == '/dev/shm' || event.exepath.startsWith('/dev/shm/')) || - (event.cwd == '/dev/shm' || event.cwd.startsWith('/dev/shm/') || + (event.cwd == '/dev/shm' || event.cwd.startsWith('/dev/shm/') || (parse.get_exec_path(event.args, event.comm).startsWith('/dev/shm/'))) profileDependency: 2 severity: 8 @@ -288,6 +296,7 @@ spec: - "exec" - "signature" - "malicious" + - "context:kubernetes" - name: "Drifted process executed" enabled: true id: "R1001" @@ -313,6 +322,7 @@ spec: - "binary" - "base image" - "applicationprofile" + - "context:kubernetes" - name: "Process tries to load a kernel module" enabled: true id: "R1002" @@ -334,6 +344,7 @@ spec: - "kernel" - "module" - "load" + - "context:kubernetes" - name: "Disallowed ssh connection" enabled: false id: "R1003" @@ -356,13 +367,14 @@ spec: - "port" - "malicious" - "networkprofile" + - "context:kubernetes" - name: "Process executed from mount" enabled: true id: "R1004" description: "Detecting exec calls from mounted paths." expressions: message: "'Process (' + event.comm + ') was executed from a mounted path'" - uniqueId: "event.comm + '_' + event.exepath + '_'" + uniqueId: "event.comm" ruleExpression: - eventType: "exec" expression: "!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) && k8s.get_container_mount_paths(event.namespace, event.podName, event.containerName).exists(mount, event.exepath.startsWith(mount) || parse.get_exec_path(event.args, event.comm).startsWith(mount))" @@ -376,6 +388,7 @@ spec: - "exec" - "mount" - "applicationprofile" + - "context:kubernetes" - name: "Fileless execution detected" enabled: true id: "R1005" @@ -396,6 +409,7 @@ spec: - "fileless" - "execution" - "malicious" + - "context:kubernetes" - name: "Process tries to escape container" enabled: true id: "R1006" @@ -405,7 +419,7 @@ spec: uniqueId: "event.comm + '_' + 'unshare'" ruleExpression: - eventType: "unshare" - expression: "!ap.was_syscall_used(event.containerId, 'unshare')" + expression: "event.pcomm != 'runc' && !ap.was_syscall_used(event.containerId, 'unshare')" profileDependency: 2 severity: 5 supportPolicy: false @@ -418,6 +432,7 @@ spec: - "unshare" - "anomaly" - "applicationprofile" + - "context:kubernetes" - name: "Crypto miner launched" enabled: true id: "R1007" @@ -438,6 +453,7 @@ spec: - "crypto" - "miners" - "malicious" + - "context:kubernetes" - name: "Crypto Mining Domain Communication" enabled: true id: "R1008" @@ -447,7 +463,7 @@ spec: uniqueId: "event.name + '_' + event.comm" ruleExpression: - eventType: "dns" - expression: "event.name in ['2cryptocalc.com.', '2miners.com.', 'antpool.com.', 'asia1.ethpool.org.', 'bohemianpool.com.', 'botbox.dev.', 'btm.antpool.com.', 'c3pool.com.', 'c4pool.org.', 'ca.minexmr.com.', 'cn.stratum.slushpool.com.', 'dash.antpool.com.', 'data.miningpoolstats.stream.', 'de.minexmr.com.', 'eth-ar.dwarfpool.com.', 'eth-asia.dwarfpool.com.', 'eth-asia1.nanopool.org.', 'eth-au.dwarfpool.com.', 'eth-au1.nanopool.org.', 'eth-br.dwarfpool.com.', 'eth-cn.dwarfpool.com.', 'eth-cn2.dwarfpool.com.', 'eth-eu.dwarfpool.com.', 'eth-eu1.nanopool.org.', 'eth-eu2.nanopool.org.', 'eth-hk.dwarfpool.com.', 'eth-jp1.nanopool.org.', 'eth-ru.dwarfpool.com.', 'eth-ru2.dwarfpool.com.', 'eth-sg.dwarfpool.com.', 'eth-us-east1.nanopool.org.', 'eth-us-west1.nanopool.org.', 'eth-us.dwarfpool.com.', 'eth-us2.dwarfpool.com.', 'eth.antpool.com.', 'eu.stratum.slushpool.com.', 'eu1.ethermine.org.', 'eu1.ethpool.org.', 'fastpool.xyz.', 'fr.minexmr.com.', 'kriptokyng.com.', 'mine.moneropool.com.', 'mine.xmrpool.net.', 'miningmadness.com.', 'monero.cedric-crispin.com.', 'monero.crypto-pool.fr.', 'monero.fairhash.org.', 'monero.hashvault.pro.', 'monero.herominers.com.', 'monerod.org.', 'monerohash.com.', 'moneroocean.stream.', 'monerop.com.', 'multi-pools.com.', 'p2pool.io.', 'pool.kryptex.com.', 'pool.minexmr.com.', 'pool.monero.hashvault.pro.', 'pool.rplant.xyz.', 'pool.supportxmr.com.', 'pool.xmr.pt.', 'prohashing.com.', 'rx.unmineable.com.', 'sg.minexmr.com.', 'sg.stratum.slushpool.com.', 'skypool.org.', 'solo-xmr.2miners.com.', 'ss.antpool.com.', 'stratum-btm.antpool.com.', 'stratum-dash.antpool.com.', 'stratum-eth.antpool.com.', 'stratum-ltc.antpool.com.', 'stratum-xmc.antpool.com.', 'stratum-zec.antpool.com.', 'stratum.antpool.com.', 'supportxmr.com.', 'trustpool.cc.', 'us-east.stratum.slushpool.com.', 'us1.ethermine.org.', 'us1.ethpool.org.', 'us2.ethermine.org.', 'us2.ethpool.org.', 'web.xmrpool.eu.', 'www.domajorpool.com.', 'www.dxpool.com.', 'www.mining-dutch.nl.', 'xmc.antpool.com.', 'xmr-asia1.nanopool.org.', 'xmr-au1.nanopool.org.', 'xmr-eu1.nanopool.org.', 'xmr-eu2.nanopool.org.', 'xmr-jp1.nanopool.org.', 'xmr-us-east1.nanopool.org.', 'xmr-us-west1.nanopool.org.', 'xmr.2miners.com.', 'xmr.crypto-pool.fr.', 'xmr.gntl.uk.', 'xmr.nanopool.org.', 'xmr.pool-pay.com.', 'xmr.pool.minergate.com.', 'xmr.solopool.org.', 'xmr.volt-mine.com.', 'xmr.zeropool.io.', 'zec.antpool.com.', 'zergpool.com.', 'auto.c3pool.org.', 'us.monero.herominers.com.']" + expression: "event.name in ['2cryptocalc.com.', '2miners.com.', 'antpool.com.', 'asia1.ethpool.org.', 'bohemianpool.com.', 'botbox.dev.', 'btm.antpool.com.', 'c3pool.com.', 'c4pool.org.', 'ca.minexmr.com.', 'cn.stratum.slushpool.com.', 'dash.antpool.com.', 'data.miningpoolstats.stream.', 'de.minexmr.com.', 'eth-ar.dwarfpool.com.', 'eth-asia.dwarfpool.com.', 'eth-asia1.nanopool.org.', 'eth-au.dwarfpool.com.', 'eth-au1.nanopool.org.', 'eth-br.dwarfpool.com.', 'eth-cn.dwarfpool.com.', 'eth-cn2.dwarfpool.com.', 'eth-eu.dwarfpool.com.', 'eth-eu1.nanopool.org.', 'eth-eu2.nanopool.org.', 'eth-hk.dwarfpool.com.', 'eth-jp1.nanopool.org.', 'eth-ru.dwarfpool.com.', 'eth-ru2.dwarfpool.com.', 'eth-sg.dwarfpool.com.', 'eth-us-east1.nanopool.org.', 'eth-us-west1.nanopool.org.', 'eth-us.dwarfpool.com.', 'eth-us2.dwarfpool.com.', 'eth.antpool.com.', 'eu.stratum.slushpool.com.', 'eu1.ethermine.org.', 'eu1.ethpool.org.', 'fastpool.xyz.', 'fr.minexmr.com.', 'kriptokyng.com.', 'mine.moneropool.com.', 'mine.xmrpool.net.', 'miningmadness.com.', 'monero.cedric-crispin.com.', 'monero.crypto-pool.fr.', 'monero.fairhash.org.', 'monero.hashvault.pro.', 'monero.herominers.com.', 'monerod.org.', 'monerohash.com.', 'moneroocean.stream.', 'monerop.com.', 'multi-pools.com.', 'p2pool.io.', 'pool.kryptex.com.', 'pool.minexmr.com.', 'pool.monero.hashvault.pro.', 'pool.rplant.xyz.', 'pool.supportxmr.com.', 'pool.xmr.pt.', 'prohashing.com.', 'rx.unmineable.com.', 'sg.minexmr.com.', 'sg.stratum.slushpool.com.', 'skypool.org.', 'solo-xmr.2miners.com.', 'ss.antpool.com.', 'stratum-btm.antpool.com.', 'stratum-dash.antpool.com.', 'stratum-eth.antpool.com.', 'stratum-ltc.antpool.com.', 'stratum-xmc.antpool.com.', 'stratum-zec.antpool.com.', 'stratum.antpool.com.', 'supportxmr.com.', 'trustpool.cc.', 'us-east.stratum.slushpool.com.', 'us1.ethermine.org.', 'us1.ethpool.org.', 'us2.ethermine.org.', 'us2.ethpool.org.', 'web.xmrpool.eu.', 'www.domajorpool.com.', 'www.dxpool.com.', 'www.mining-dutch.nl.', 'xmc.antpool.com.', 'xmr-asia1.nanopool.org.', 'xmr-au1.nanopool.org.', 'xmr-eu1.nanopool.org.', 'xmr-eu2.nanopool.org.', 'xmr-jp1.nanopool.org.', 'xmr-us-east1.nanopool.org.', 'xmr-us-west1.nanopool.org.', 'xmr.2miners.com.', 'xmr.crypto-pool.fr.', 'xmr.gntl.uk.', 'xmr.nanopool.org.', 'xmr.pool-pay.com.', 'xmr.pool.minergate.com.', 'xmr.solopool.org.', 'xmr.volt-mine.com.', 'xmr.zeropool.io.', 'zec.antpool.com.', 'zergpool.com.', 'auto.c3pool.org.', 'us.monero.herominers.com.', 'xmr.kryptex.network.']" profileDependency: 2 severity: 10 supportPolicy: false @@ -460,6 +476,7 @@ spec: - "miners" - "malicious" - "dns" + - "context:kubernetes" - name: "Crypto Mining Related Port Communication" enabled: true id: "R1009" @@ -482,6 +499,7 @@ spec: - "miners" - "malicious" - "networkprofile" + - "context:kubernetes" - name: "Soft link created over sensitive file" enabled: true id: "R1010" @@ -502,6 +520,7 @@ spec: - "anomaly" - "symlink" - "applicationprofile" + - "context:kubernetes" - name: "ld_preload hooks technique detected" enabled: false id: "R1011" @@ -524,6 +543,7 @@ spec: - "exec" - "malicious" - "applicationprofile" + - "context:kubernetes" - name: "Hard link created over sensitive file" enabled: true id: "R1012" @@ -544,6 +564,7 @@ spec: - "files" - "malicious" - "applicationprofile" + - "context:kubernetes" - name: "Malicious Ptrace Usage" enabled: true id: "R1015" @@ -563,6 +584,7 @@ spec: tags: - "process" - "malicious" + - "context:kubernetes" - name: "Unexpected io_uring Operation Detected" enabled: true id: "R1030" @@ -583,3 +605,22 @@ spec: - "syscalls" - "io_uring" - "applicationprofile" + - "context:kubernetes" + - name: "Signed profile tampered" + enabled: true + id: "R1016" + description: "Detects when a previously signed ApplicationProfile or NetworkNeighborhood has been tampered with (signature no longer valid)." + expressions: + message: "'Signed profile tampered'" + uniqueId: "'R1016'" + ruleExpression: [] + profileDependency: 2 + severity: 10 + supportPolicy: false + isTriggerAlert: false + mitreTactic: "TA0005" + mitreTechnique: "T1565" + tags: + - "integrity" + - "signature" + - "tamper" diff --git a/tests/chart/values.yaml b/tests/chart/values.yaml index 7cf029c4c8..cde97df906 100644 --- a/tests/chart/values.yaml +++ b/tests/chart/values.yaml @@ -32,7 +32,7 @@ global: storage: name: "storage" image: - repository: quay.io/kubescape/storage + repository: ghcr.io/k8sstormcenter/storage tag: v0.0.156 pullPolicy: Always cleanupInterval: "6h" @@ -50,7 +50,7 @@ storage: nodeAgent: name: node-agent image: - repository: quay.io/kubescape/node-agent + repository: ghcr.io/k8sstormcenter/node-agent tag: v0.2.21 pullPolicy: IfNotPresent diff --git a/tests/component_test.go b/tests/component_test.go index 380f325765..7da09b44e0 100644 --- a/tests/component_test.go +++ b/tests/component_test.go @@ -8,9 +8,11 @@ import ( "fmt" "path" "reflect" + "runtime" "slices" "sort" "strconv" + "strings" "testing" "time" @@ -18,6 +20,8 @@ import ( "github.com/kubescape/go-logger/helpers" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/k8s-interface/k8sinterface" + "github.com/kubescape/node-agent/pkg/signature" + "github.com/kubescape/node-agent/pkg/signature/profiles" "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/node-agent/tests/testutils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" @@ -27,7 +31,10 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" "k8s.io/utils/ptr" ) @@ -494,53 +501,246 @@ func Test_09_FalsePositiveTest(t *testing.T) { assert.Equal(t, 0, len(alerts), "Expected no alerts to be generated, but got %d alerts", len(alerts)) } +// Test_10_CryptoMinerDetection tests crypto-miner detection from two angles: +// - malware_scan: ClamAV file-scanning detects xmrig binary signature +// - empty_profile_rules: empty user-defined AP means every exec/DNS is anomalous, +// so rule-based detection fires immediately without a learning period func Test_10_MalwareDetectionTest(t *testing.T) { start := time.Now() defer tearDownTest(t, start) - t.Log("Creating namespace") - ns := testutils.NewRandomNamespace() + // --------------------------------------------------------------- + // 10a. Malware file-scanning (ClamAV signature match) + // --------------------------------------------------------------- + t.Run("malware_scan", func(t *testing.T) { + ns := testutils.NewRandomNamespace() - t.Log("Deploy container with malware") - exitCode := testutils.RunCommand("kubectl", "run", "-n", ns.Name, "malware-cryptominer", "--image=quay.io/petr_ruzicka/malware-cryptominer-container:2.0.2") - require.Equalf(t, 0, exitCode, "expected no error when deploying malware container") + t.Log("Deploy container with malware") + exitCode := testutils.RunCommand("kubectl", "run", "-n", ns.Name, "malware-cryptominer", "--image=quay.io/petr_ruzicka/malware-cryptominer-container:2.0.2") + require.Equalf(t, 0, exitCode, "expected no error when deploying malware container") - // Wait for pod to be ready - exitCode = testutils.RunCommand("kubectl", "wait", "--for=condition=Ready", "pod", "malware-cryptominer", "-n", ns.Name, "--timeout=300s") - require.Equalf(t, 0, exitCode, "expected no error when waiting for pod to be ready") + exitCode = testutils.RunCommand("kubectl", "wait", "--for=condition=Ready", "pod", "malware-cryptominer", "-n", ns.Name, "--timeout=300s") + require.Equalf(t, 0, exitCode, "expected no error when waiting for pod to be ready") - // wait for application profile to be completed - time.Sleep(3 * time.Minute) + // Wait for application profile to be completed. + time.Sleep(3 * time.Minute) - _, _, err := testutils.ExecIntoPod("malware-cryptominer", ns.Name, []string{"ls", "-l", "/usr/share/nginx/html/xmrig"}, "") - require.NoErrorf(t, err, "expected no error when executing command in malware container") + _, _, err := testutils.ExecIntoPod("malware-cryptominer", ns.Name, []string{"ls", "-l", "/usr/share/nginx/html/xmrig"}, "") + require.NoErrorf(t, err, "expected no error when executing command in malware container") - _, _, err = testutils.ExecIntoPod("malware-cryptominer", ns.Name, []string{"/usr/share/nginx/html/xmrig/xmrig"}, "") + _, _, err = testutils.ExecIntoPod("malware-cryptominer", ns.Name, []string{"/usr/share/nginx/html/xmrig/xmrig"}, "") - // wait for the alerts to be generated - time.Sleep(20 * time.Second) + time.Sleep(20 * time.Second) - alerts, err := testutils.GetMalwareAlerts(ns.Name) - require.NoError(t, err, "Error getting alerts") + alerts, err := testutils.GetMalwareAlerts(ns.Name) + require.NoError(t, err, "Error getting alerts") - expectedMalwares := []string{ - "Multios.Coinminer.Miner-6781728-2.UNOFFICIAL", - } + expectedMalwares := []string{ + "Multios.Coinminer.Miner-6781728-2.UNOFFICIAL", + } - malwaresDetected := map[string]bool{} + malwaresDetected := map[string]bool{} + for _, alert := range alerts { + podName, podNameOk := alert.Labels["pod_name"] + malwareName, malwareNameOk := alert.Labels["malware_name"] + if podNameOk && malwareNameOk { + if podName == "malware-cryptominer" && slices.Contains(expectedMalwares, malwareName) { + malwaresDetected[malwareName] = true + } + } + } - for _, alert := range alerts { - podName, podNameOk := alert.Labels["pod_name"] - malwareName, malwareNameOk := alert.Labels["malware_name"] + assert.Equal(t, len(expectedMalwares), len(malwaresDetected), + "Expected %d malwares to be detected, but got %d", len(expectedMalwares), len(malwaresDetected)) + }) + + // --------------------------------------------------------------- + // 10b. Behavioral rule detection with empty user-defined AP. + // The miner starts immediately; because the AP declares nothing, + // every exec, DNS lookup, and network connection is anomalous. + // + // Expected rules: + // R0001: Unexpected process launched (every exec) + // R0003: Syscalls Anomalies (empty syscall list) + // + // Rules that MAY fire depending on network conditions: + // R0005: DNS Anomalies (requires DNS responses with answers; + // trace_dns drops NXDOMAIN, so behind a firewall these + // won't arrive) + // R1008: Crypto Mining Domain Communication (same DNS dependency) + // R1009: Crypto Mining Related Port Communication (requires TCP + // connectivity to mining pool ports 3333/45700) + // R1007: Crypto miner launched via randomx (amd64 only) + // + // Race condition note: the node-agent fetches the user-defined AP + // from storage asynchronously after detecting the container. Events + // arriving before the fetch completes see profileExists=false, + // causing Required rules (R0001 etc.) to be skipped. The miner's + // initial exec happens during this window — so we must exec into + // the pod AFTER the profile is cached to generate observable exec + // events. + // --------------------------------------------------------------- + t.Run("empty_profile_rules", func(t *testing.T) { + ns := testutils.NewRandomNamespace() + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + + // Create an ApplicationProfile with an empty container entry for k8s-miner. + // The container name must match the pod's container so + // GetContainerFromApplicationProfile finds it. With no execs, syscalls, + // opens, or capabilities listed, every operation is anomalous. + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "crypto2", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + {Name: "k8s-miner"}, + }, + }, + } - if podNameOk && malwareNameOk { - if podName == "malware-cryptominer" && slices.Contains(expectedMalwares, malwareName) { - malwaresDetected[malwareName] = true + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), ap, metav1.CreateOptions{}) + require.NoError(t, err, "create empty AP in storage") + + require.Eventually(t, func() bool { + _, getErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), "crypto2", v1.GetOptions{}) + return getErr == nil + }, 30*time.Second, 1*time.Second, "empty AP must be stored") + + // Deploy crypto miner with user-defined profile label. + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/crypto-miner-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + t.Log("Crypto miner pod is ready") + + // Wait for node-agent to fetch the user-defined AP from storage and + // cache it. The miner's initial execve races with this fetch, so + // R0001 is skipped for that event. Syscalls keep flowing, so R0003 + // fires once the profile is cached. + time.Sleep(20 * time.Second) + + // Exec into the pod to generate post-profile-load events: + // exec event → R0001 (cat not in empty AP) + // open event → R0002 (/etc/hostname starts with /etc/) + stdout, stderr, execErr := wl.ExecIntoPod([]string{"cat", "/etc/hostname"}, "k8s-miner") + t.Logf("exec cat /etc/hostname: err=%v stdout=%q stderr=%q", execErr, stdout, stderr) + + // Collect alerts — R0001 must appear from the exec above. + var alerts []testutils.Alert + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(ns.Name) + if err != nil || len(alerts) == 0 { + return false } + for _, a := range alerts { + if a.Labels["rule_id"] == "R0001" { + return true + } + } + return false + }, 120*time.Second, 10*time.Second, "expected R0001 alert from exec with empty AP") + + time.Sleep(15 * time.Second) + alerts, _ = testutils.GetAlerts(ns.Name) + + t.Logf("=== %d alerts ===", len(alerts)) + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + + rulesSeen := map[string]bool{} + for _, a := range alerts { + rulesSeen[a.Labels["rule_id"]] = true } - } - assert.Equal(t, len(expectedMalwares), len(malwaresDetected), "Expected %d malwares to be detected, but got %d malwares", len(expectedMalwares), len(malwaresDetected)) + // These rules must fire with an empty AP — every operation is anomalous. + assert.True(t, rulesSeen["R0001"], + "R0001 (Unexpected process launched) must fire — cat exec not in empty AP") + assert.True(t, rulesSeen["R0002"], + "R0002 (Files Access Anomalies) must fire — /etc/hostname not in empty AP opens") + assert.True(t, rulesSeen["R0003"], + "R0003 (Syscalls Anomalies) must fire — miner syscalls not in empty AP") + assert.True(t, rulesSeen["R0004"], + "R0004 (Linux Capabilities Anomalies) must fire — capabilities not in empty AP") + + // DNS/network rules depend on the miner resolving pool domains and + // establishing TCP connections. In sandboxed/firewalled environments + // these won't fire: trace_dns drops NXDOMAIN, and TCP to mining + // ports is blocked. Log what fired for visibility. + for _, entry := range []struct { + id, desc string + }{ + {"R0005", "DNS Anomalies"}, + {"R1007", "Crypto miner launched via randomx"}, + {"R1008", "Crypto Mining Domain Communication"}, + {"R1009", "Crypto Mining Related Port Communication"}, + } { + if rulesSeen[entry.id] { + t.Logf("%s (%s) fired", entry.id, entry.desc) + } + } + }) + + // --------------------------------------------------------------- + // 10c. RandomX detection (R1007) via xmrig benchmark mode. + // Uses --bench 1M which runs RandomX hashing without a pool + // connection, reliably triggering the x86 FPU tracepoint + // that the randomx eBPF gadget monitors. + // x86_64 (amd64) only — the gadget is disabled on arm64. + // --------------------------------------------------------------- + t.Run("randomx_bench", func(t *testing.T) { + if runtime.GOARCH != "amd64" { + t.Skip("randomx tracer is x86_64 only") + } + + ns := testutils.NewRandomNamespace() + + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/crypto-miner-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + t.Log("xmrig benchmark pod is ready, waiting for RandomX FPU events...") + + // xmrig needs ~5s to init the RandomX dataset, then starts hashing. + // The eBPF gadget needs 5 FPU events within 5s to fire. + // Give it 30s total. + var alerts []testutils.Alert + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(ns.Name) + if err != nil || len(alerts) == 0 { + return false + } + for _, a := range alerts { + if a.Labels["rule_id"] == "R1007" { + return true + } + } + return false + }, 120*time.Second, 10*time.Second, "expected R1007 (RandomX crypto miner) from xmrig --bench") + + alerts, _ = testutils.GetAlerts(ns.Name) + t.Logf("=== %d alerts ===", len(alerts)) + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + + rulesSeen := map[string]bool{} + for _, a := range alerts { + rulesSeen[a.Labels["rule_id"]] = true + } + + assert.True(t, rulesSeen["R1007"], + "R1007 (Crypto miner launched via randomx) must fire — xmrig benchmark runs RandomX hashing") + }) } func Test_11_EndpointTest(t *testing.T) { @@ -1565,3 +1765,1971 @@ func Test_24_ProcessTreeDepthTest(t *testing.T) { t.Logf("Found alerts for the process tree depth: %v", alerts) } + +// Test_27_ApplicationProfileOpens tests that the dynamic path matching in +// application profiles works correctly for both recorded (auto-learned) +// profiles and user-defined profiles. +// +// Path matching symbols: +// +// ⋯ (U+22EF DynamicIdentifier) — matches exactly ONE path segment +// * (WildcardIdentifier) — matches ZERO or more path segments +// 0 (in endpoints) — wildcard port (any port) +// +// R0002 "Files Access Anomalies in container" fires when a file is opened +// under a monitored prefix (/etc/, /var/log/, …) and the path was NOT +// recorded in the application profile. +func Test_27_ApplicationProfileOpens(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + const ruleName = "Files Access Anomalies in container" + const profileName = "nginx-regex-profile" + + // --- result tracking for end-of-test summary --- + type subtestResult struct { + name string + profilePath string + filePath string + expectAlert bool + passed bool + detail string + } + var results []subtestResult + addResult := func(name, profilePath, filePath string, expectAlert, passed bool, detail string) { + results = append(results, subtestResult{name, profilePath, filePath, expectAlert, passed, detail}) + } + defer func() { + t.Log("\n========== Test_27 Summary ==========") + anyFailed := false + for _, r := range results { + status := "PASS" + if !r.passed { + status = "FAIL" + anyFailed = true + } + expect := "expect alert" + if !r.expectAlert { + expect = "expect NO alert" + } + t.Logf(" [%s] %-35s profile=%-25s file=%-25s %s", status, r.name, r.profilePath, r.filePath, expect) + if !r.passed { + t.Logf(" -> %s", r.detail) + } + } + if !anyFailed { + t.Log(" All subtests passed.") + } + t.Log("======================================") + }() + + // deployWithProfile creates a user-defined ApplicationProfile with the + // given Opens list, polls until it is retrievable from storage, then + // deploys nginx with the kubescape.io/user-defined-profile label + // pointing at it, and waits for the pod to be ready. + deployWithProfile := func(t *testing.T, opens []v1beta1.OpenCalls) *testutils.TestWorkload { + t.Helper() + ns := testutils.NewRandomNamespace() + + profile := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: profileName, + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64"}, + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "nginx", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/cat", Args: []string{"/bin/cat"}}, + }, + Opens: opens, + }, + }, + }, + } + + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), profile, metav1.CreateOptions{}) + require.NoError(t, err, "create user-defined profile %q in ns %s", profileName, ns.Name) + + // Poll until the profile is retrievable from storage before deploying. + // Node-agent does a single fetch on container start with no retry. + require.Eventually(t, func() bool { + _, apErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), profileName, v1.GetOptions{}) + return apErr == nil + }, 30*time.Second, 1*time.Second, "AP must be retrievable from storage before deploying the pod") + + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/nginx-user-profile-deployment.yaml")) + require.NoError(t, err, "create workload in ns %s", ns.Name) + require.NoError(t, wl.WaitForReady(80), "workload not ready in ns %s", ns.Name) + + // Wait for node-agent to load the user-defined profile into cache. + time.Sleep(10 * time.Second) + return wl + } + + // triggerAndGetAlerts execs cat on the given path, then polls for alerts + // up to 60s to avoid race conditions with alert propagation. + triggerAndGetAlerts := func(t *testing.T, wl *testutils.TestWorkload, filePath string) []testutils.Alert { + t.Helper() + stdout, stderr, err := wl.ExecIntoPod([]string{"cat", filePath}, "nginx") + if err != nil { + t.Errorf("exec 'cat %s' in container nginx failed: %v (stdout=%q stderr=%q)", filePath, err, stdout, stderr) + } + // Poll for alerts — they may take time to propagate through + // eBPF → node-agent → alertmanager. + var alerts []testutils.Alert + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(wl.Namespace) + return err == nil + }, 60*time.Second, 5*time.Second, "alerts must be retrievable from ns %s", wl.Namespace) + // Give extra time for all alerts to arrive after first successful fetch. + time.Sleep(10 * time.Second) + alerts, err = testutils.GetAlerts(wl.Namespace) + require.NoError(t, err, "get alerts from ns %s", wl.Namespace) + return alerts + } + + // hasAlert checks whether an R0002 alert exists for comm=cat, container=nginx. + hasAlert := func(alerts []testutils.Alert) bool { + for _, a := range alerts { + if a.Labels["rule_name"] == ruleName && + a.Labels["comm"] == "cat" && + a.Labels["container_name"] == "nginx" { + return true + } + } + return false + } + + // --------------------------------------------------------------- + // 1a. Recorded (auto-learned) profile must use absolute paths. + // There must be no "." in the Opens paths. + // --------------------------------------------------------------- + t.Run("recorded_profile_absolute_paths", func(t *testing.T) { + ns := testutils.NewRandomNamespace() + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/nginx-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + require.NoError(t, wl.WaitForApplicationProfileCompletion(80)) + + profile, err := wl.GetApplicationProfile() + require.NoError(t, err, "get application profile") + + passed := true + for _, container := range profile.Spec.Containers { + for _, open := range container.Opens { + if !strings.HasPrefix(open.Path, "/") { + t.Errorf("recorded path must be absolute: got %q (container %s)", open.Path, container.Name) + passed = false + } + if open.Path == "." { + t.Errorf("recorded path must not be relative dot: got %q (container %s)", open.Path, container.Name) + passed = false + } + } + } + detail := "" + if !passed { + detail = "found non-absolute or '.' paths in recorded profile" + } + addResult("recorded_profile_absolute_paths", "(auto-learned)", "(nginx startup)", false, passed, detail) + }) + + // --------------------------------------------------------------- + // 1b. User-defined profile wildcard tests. + // Each sub-test deploys nginx in its own namespace with a + // different Opens pattern and verifies R0002 behaviour. + // --------------------------------------------------------------- + + // 1b-1: Exact path — profile has the exact file => no alert. + t.Run("exact_path_match", func(t *testing.T) { + profilePath := "/etc/nginx/nginx.conf" + filePath := "/etc/nginx/nginx.conf" + wl := deployWithProfile(t, []v1beta1.OpenCalls{ + {Path: profilePath, Flags: []string{"O_RDONLY"}}, + {Path: "/etc/ld.so.cache", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, // dynamic linker opens this on every exec + }) + alerts := triggerAndGetAlerts(t, wl, filePath) + got := hasAlert(alerts) + if got { + t.Errorf("expected NO R0002 alert: profile allows %q, opened %q, but alert fired", profilePath, filePath) + } + addResult("exact_path_match", profilePath, filePath, false, !got, + fmt.Sprintf("got %d alerts, expected none for cat", len(alerts))) + }) + + // 1b-2: Exact path — profile has a DIFFERENT file => alert. + t.Run("exact_path_mismatch", func(t *testing.T) { + profilePath := "/etc/nginx/nginx.conf" + filePath := "/etc/hostname" + wl := deployWithProfile(t, []v1beta1.OpenCalls{ + {Path: profilePath, Flags: []string{"O_RDONLY"}}, + }) + alerts := triggerAndGetAlerts(t, wl, filePath) + got := hasAlert(alerts) + if !got { + t.Errorf("expected R0002 alert: profile only allows %q, opened %q, but no alert", profilePath, filePath) + } + addResult("exact_path_mismatch", profilePath, filePath, true, got, + fmt.Sprintf("got %d alerts, expected at least one for cat", len(alerts))) + }) + + // 1b-3: Ellipsis ⋯ matches single segment — /etc/⋯ covers /etc/hostname. + t.Run("ellipsis_single_segment_match", func(t *testing.T) { + profilePath := "/etc/" + dynamicpathdetector.DynamicIdentifier + filePath := "/etc/hostname" + wl := deployWithProfile(t, []v1beta1.OpenCalls{ + {Path: profilePath, Flags: []string{"O_RDONLY"}}, + }) + alerts := triggerAndGetAlerts(t, wl, filePath) + got := hasAlert(alerts) + if got { + t.Errorf("expected NO R0002 alert: profile %q should match %q (single segment), but alert fired", profilePath, filePath) + } + addResult("ellipsis_single_segment_match", profilePath, filePath, false, !got, + fmt.Sprintf("got %d alerts, expected none for cat", len(alerts))) + }) + + // 1b-4: Ellipsis ⋯ rejects multi-segment — /etc/⋯ does NOT cover + // /etc/nginx/nginx.conf (two segments past /etc/). + t.Run("ellipsis_rejects_multi_segment", func(t *testing.T) { + profilePath := "/etc/" + dynamicpathdetector.DynamicIdentifier + filePath := "/etc/nginx/nginx.conf" + wl := deployWithProfile(t, []v1beta1.OpenCalls{ + {Path: profilePath, Flags: []string{"O_RDONLY"}}, + }) + alerts := triggerAndGetAlerts(t, wl, filePath) + got := hasAlert(alerts) + if !got { + t.Errorf("expected R0002 alert: profile %q should NOT match %q (two segments), but no alert", profilePath, filePath) + } + addResult("ellipsis_rejects_multi_segment", profilePath, filePath, true, got, + fmt.Sprintf("got %d alerts, expected at least one for cat", len(alerts))) + }) + + // 1b-5: Wildcard * matches any depth — /etc/* covers /etc/nginx/nginx.conf. + t.Run("wildcard_matches_deep_path", func(t *testing.T) { + profilePath := "/etc/*" + filePath := "/etc/nginx/nginx.conf" + wl := deployWithProfile(t, []v1beta1.OpenCalls{ + {Path: profilePath, Flags: []string{"O_RDONLY"}}, + }) + alerts := triggerAndGetAlerts(t, wl, filePath) + got := hasAlert(alerts) + if got { + t.Errorf("expected NO R0002 alert: profile %q should match %q (wildcard), but alert fired", profilePath, filePath) + } + addResult("wildcard_matches_deep_path", profilePath, filePath, false, !got, + fmt.Sprintf("got %d alerts, expected none for cat", len(alerts))) + }) + + // --------------------------------------------------------------- + // 1c. Deploy known-application-profile-wildcards.yaml (curl image) + // and verify that files under wildcard-covered opens paths + // produce no R0002 alert. + // --------------------------------------------------------------- + t.Run("wildcard_yaml_profile_allowed_opens", func(t *testing.T) { + ns := testutils.NewRandomNamespace() + wildcardProfileName := "fusioncore-profile-wildcards" + + // Create the profile matching known-application-profile-wildcards.yaml. + profile := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: wildcardProfileName, + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Architectures: []string{"amd64"}, + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + ImageID: "docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058", + ImageTag: "docker.io/curlimages/curl:8.5.0", + Capabilities: []string{ + "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", + "CAP_SETGID", "CAP_SETPCAP", "CAP_SETUID", "CAP_SYS_ADMIN", + }, + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep", Args: []string{"/bin/sleep", "infinity"}}, + {Path: "/bin/cat", Args: []string{"/bin/cat"}}, + {Path: "/usr/bin/curl", Args: []string{"/usr/bin/curl", "-sm2", "fusioncore.ai"}}, + }, + Opens: []v1beta1.OpenCalls{ + {Path: "/etc/*", Flags: []string{"O_RDONLY", "O_LARGEFILE", "O_CLOEXEC"}}, + {Path: "/etc/ssl/openssl.cnf", Flags: []string{"O_RDONLY", "O_LARGEFILE"}}, + {Path: "/home/*", Flags: []string{"O_RDONLY", "O_LARGEFILE"}}, + {Path: "/lib/*", Flags: []string{"O_RDONLY", "O_LARGEFILE", "O_CLOEXEC"}}, + {Path: "/usr/lib/*", Flags: []string{"O_RDONLY", "O_LARGEFILE", "O_CLOEXEC"}}, + {Path: "/usr/local/lib/*", Flags: []string{"O_RDONLY", "O_LARGEFILE", "O_CLOEXEC"}}, + {Path: "/proc/*/cgroup", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + {Path: "/proc/*/kernel/cap_last_cap", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + {Path: "/proc/*/mountinfo", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + {Path: "/proc/*/task/*/fd", Flags: []string{"O_RDONLY", "O_DIRECTORY", "O_CLOEXEC"}}, + {Path: "/sys/fs/cgroup/cpu.max", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + {Path: "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", Flags: []string{"O_RDONLY"}}, + {Path: "/7/setgroups", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + {Path: "/runc", Flags: []string{"O_RDONLY", "O_CLOEXEC"}}, + }, + Syscalls: []string{ + "arch_prctl", "bind", "brk", "capget", "capset", "chdir", + "clone", "close", "close_range", "connect", "epoll_ctl", + "epoll_pwait", "execve", "exit", "exit_group", "faccessat2", + "fchown", "fcntl", "fstat", "fstatfs", "futex", "getcwd", + "getdents64", "getegid", "geteuid", "getgid", "getpeername", + "getppid", "getsockname", "getsockopt", "gettid", "getuid", + "ioctl", "membarrier", "mmap", "mprotect", "munmap", + "nanosleep", "newfstatat", "open", "openat", "openat2", + "pipe", "poll", "prctl", "read", "recvfrom", "recvmsg", + "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sendto", + "set_tid_address", "setgid", "setgroups", "setsockopt", + "setuid", "sigaltstack", "socket", "statx", "tkill", + "unknown", "write", "writev", + }, + }, + }, + }, + } + + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), profile, metav1.CreateOptions{}) + require.NoError(t, err, "create wildcard profile %q in ns %s", wildcardProfileName, ns.Name) + + // Poll until the profile is retrievable from storage before deploying. + require.Eventually(t, func() bool { + _, apErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), wildcardProfileName, v1.GetOptions{}) + return apErr == nil + }, 30*time.Second, 1*time.Second, "AP must be retrievable before deploying the pod") + + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/curl-user-profile-wildcards-deployment.yaml")) + require.NoError(t, err, "create curl workload in ns %s", ns.Name) + require.NoError(t, wl.WaitForReady(80), "curl workload not ready in ns %s", ns.Name) + + // Wait for node-agent to load the user-defined profile into cache. + time.Sleep(10 * time.Second) + + // Cat files that are covered by the wildcard opens. + allowedFiles := []string{ + "/etc/hosts", // covered by /etc/* + "/etc/resolv.conf", // covered by /etc/* + "/etc/ssl/openssl.cnf", // exact match + } + for _, f := range allowedFiles { + stdout, stderr, err := wl.ExecIntoPod([]string{"cat", f}, "curl") + if err != nil { + t.Logf("exec 'cat %s' failed: %v (stdout=%q stderr=%q)", f, err, stdout, stderr) + } + } + + // Poll for alerts to propagate. + time.Sleep(15 * time.Second) + alerts, err := testutils.GetAlerts(wl.Namespace) + require.NoError(t, err, "get alerts from ns %s", wl.Namespace) + + var r0002Fired bool + for _, a := range alerts { + if a.Labels["rule_name"] == ruleName && + a.Labels["comm"] == "cat" && + a.Labels["container_name"] == "curl" { + r0002Fired = true + break + } + } + if r0002Fired { + t.Errorf("expected NO R0002 for files covered by wildcard opens, but alert fired") + } + addResult("wildcard_yaml_profile_allowed_opens", + "/etc/*, /etc/ssl/openssl.cnf", "/etc/hosts, /etc/resolv.conf, /etc/ssl/openssl.cnf", + false, !r0002Fired, + fmt.Sprintf("got R0002=%v, expected none for wildcard-covered files", r0002Fired)) + }) +} + +// Test_28_UserDefinedNetworkNeighborhood exercises user-defined AP + NN. +// Each subtest gets its own namespace to avoid alert cross-contamination. +// +// The NN allows only fusioncore.ai (162.0.217.171) on TCP/80. +// R0005 requires real resolvable domains (not NXDOMAIN), because trace_dns +// drops DNS responses with 0 answers. +func Test_28_UserDefinedNetworkNeighborhood(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + // setup creates a namespace with user-defined AP + NN + pod. + // The NN allows only fusioncore.ai (162.0.217.171) on TCP/80. + setup := func(t *testing.T) *testutils.TestWorkload { + t.Helper() + ns := testutils.NewRandomNamespace() + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "curl-ap", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep"}, + {Path: "/usr/bin/curl"}, + {Path: "/usr/bin/nslookup"}, + {Path: "/usr/bin/wget"}, + }, + Syscalls: []string{"socket", "connect", "sendto", "recvfrom", "read", "write", "close", "openat", "mmap", "mprotect", "munmap", "fcntl", "ioctl", "poll", "epoll_create1", "epoll_ctl", "epoll_wait", "bind", "listen", "accept4", "getsockopt", "setsockopt", "getsockname", "getpid", "fstat", "rt_sigaction", "rt_sigprocmask", "writev"}, + }, + }, + }, + } + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), ap, metav1.CreateOptions{}) + require.NoError(t, err, "create AP") + + nn := &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "curl-nn", + Namespace: ns.Name, + Annotations: map[string]string{ + helpersv1.ManagedByMetadataKey: helpersv1.ManagedByUserValue, + helpersv1.StatusMetadataKey: helpersv1.Completed, + helpersv1.CompletionMetadataKey: helpersv1.Full, + }, + Labels: map[string]string{ + helpersv1.ApiGroupMetadataKey: "apps", + helpersv1.ApiVersionMetadataKey: "v1", + helpersv1.KindMetadataKey: "Deployment", + helpersv1.NameMetadataKey: "curl-28", + helpersv1.NamespaceMetadataKey: ns.Name, + }, + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "curl-28"}, + }, + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "curl", + Egress: []v1beta1.NetworkNeighbor{ + { + Identifier: "fusioncore-egress", + Type: "external", + DNS: "fusioncore.ai.", + DNSNames: []string{"fusioncore.ai."}, + IPAddress: "162.0.217.171", + Ports: []v1beta1.NetworkPort{ + {Name: "TCP-80", Protocol: "TCP", Port: ptr.To(int32(80))}, + }, + }, + }, + }, + }, + }, + } + _, err = storageClient.NetworkNeighborhoods(ns.Name).Create( + context.Background(), nn, metav1.CreateOptions{}) + require.NoError(t, err, "create NN") + + require.Eventually(t, func() bool { + _, apErr := storageClient.ApplicationProfiles(ns.Name).Get(context.Background(), "curl-ap", v1.GetOptions{}) + _, nnErr := storageClient.NetworkNeighborhoods(ns.Name).Get(context.Background(), "curl-nn", v1.GetOptions{}) + return apErr == nil && nnErr == nil + }, 30*time.Second, 1*time.Second, "AP+NN must be in storage before pod deploy") + + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/nginx-user-defined-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + time.Sleep(15 * time.Second) // let node-agent load profiles + return wl + } + + waitAlerts := func(t *testing.T, ns string) []testutils.Alert { + t.Helper() + var alerts []testutils.Alert + var err error + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(ns) + return err == nil + }, 60*time.Second, 5*time.Second, "must be able to fetch alerts") + // Extra settle time for remaining alerts. + time.Sleep(10 * time.Second) + alerts, _ = testutils.GetAlerts(ns) + return alerts + } + + logAlerts := func(t *testing.T, alerts []testutils.Alert) { + t.Helper() + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + } + + // getLogAlerts fetches the rich alert data from node-agent stdout logs. + getLogAlerts := func(t *testing.T, ns string) []testutils.LogAlert { + t.Helper() + la, err := testutils.GetLogAlerts(ns) + if err != nil { + t.Logf("warning: could not get log alerts: %v", err) + return nil + } + return la + } + + // logRichAlerts logs the rich alert details (domain, IP, port, proto). + logRichAlerts := func(t *testing.T, alerts []testutils.LogAlert) { + t.Helper() + for i, a := range alerts { + extra := "" + if a.Domain != "" { + extra += " domain=" + a.Domain + } + if a.DstIP != "" { + extra += " ip=" + a.DstIP + } + if a.Port != "" { + extra += " port=" + a.Port + } + if a.Proto != "" { + extra += " proto=" + a.Proto + } + if len(a.Addresses) > 0 { + extra += fmt.Sprintf(" addrs=%v", a.Addresses) + } + t.Logf(" [%d] %s(%s) comm=%s container=%s%s", + i, a.AlertName, a.RuleID, a.Comm, a.Container, extra) + } + } + + // --------------------------------------------------------------- + // 28a. Allowed traffic — fusioncore.ai is in the NN. + // No R0005 (DNS) and no R0011 (egress) expected. + // --------------------------------------------------------------- + t.Run("allowed_fusioncore_no_alert", func(t *testing.T) { + wl := setup(t) + + // DNS lookup via nslookup (domain in NN). + stdout, stderr, err := wl.ExecIntoPod([]string{"nslookup", "fusioncore.ai"}, "curl") + t.Logf("nslookup fusioncore.ai → err=%v stdout=%q stderr=%q", err, stdout, stderr) + + // HTTP via curl (domain + IP in NN). + stdout, stderr, err = wl.ExecIntoPod([]string{"curl", "-sm5", "http://fusioncore.ai"}, "curl") + t.Logf("curl fusioncore.ai → err=%v stdout=%q stderr=%q", err, stdout, stderr) + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + t.Logf("=== %d log alerts ===", len(richAlerts)) + logRichAlerts(t, richAlerts) + + r0005 := testutils.FilterLogAlerts(richAlerts, "R0005") + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + assert.Empty(t, r0005, + "fusioncore.ai is in NN — should NOT fire R0005") + assert.Empty(t, r0011, + "fusioncore.ai IP is in NN — should NOT fire R0011") + }) + + // --------------------------------------------------------------- + // 28b. Unknown domains — domains NOT in the NN → R0005. + // Uses both nslookup (pure DNS) and curl (DNS + TCP). + // --------------------------------------------------------------- + t.Run("unknown_domain_R0005", func(t *testing.T) { + wl := setup(t) + + // nslookup generates a DNS query without any TCP connection. + wl.ExecIntoPod([]string{"nslookup", "google.com"}, "curl") + // curl resolves + connects. + wl.ExecIntoPod([]string{"curl", "-sm5", "http://ebpf.io"}, "curl") + wl.ExecIntoPod([]string{"curl", "-sm5", "http://cloudflare.com"}, "curl") + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + r0005 := testutils.FilterLogAlerts(richAlerts, "R0005") + t.Logf("=== R0005 alerts (%d) ===", len(r0005)) + logRichAlerts(t, r0005) + require.NotEmpty(t, r0005, "unknown domains must fire R0005") + + domains := testutils.LogAlertDomains(r0005) + t.Logf("R0005 anomalous domains: %v", domains) + + // Verify at least one of the queried domains triggered. + foundKnown := false + for _, d := range domains { + if strings.Contains(d, "google.com") || strings.Contains(d, "ebpf.io") || strings.Contains(d, "cloudflare.com") { + foundKnown = true + break + } + } + assert.True(t, foundKnown, + "R0005 must fire for at least one of the queried domains (google.com, ebpf.io, cloudflare.com), got: %v", domains) + }) + + // --------------------------------------------------------------- + // 28c. Unknown IPs — raw IP egress NOT in the NN → R0011. + // --------------------------------------------------------------- + t.Run("unknown_ip_R0011", func(t *testing.T) { + wl := setup(t) + + wl.ExecIntoPod([]string{"curl", "-sm5", "http://8.8.8.8"}, "curl") + wl.ExecIntoPod([]string{"curl", "-sm5", "http://1.1.1.1"}, "curl") + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + t.Logf("=== R0011 alerts (%d) ===", len(r0011)) + logRichAlerts(t, r0011) + require.NotEmpty(t, r0011, "IPs not in NN must fire R0011") + + ips := testutils.LogAlertIPs(r0011) + t.Logf("R0011 anomalous IPs: %v", ips) + + // Verify the specific IPs we curled show up. + foundGoogle := false + foundCloudflare := false + for _, ip := range ips { + if ip == "8.8.8.8" { + foundGoogle = true + } + if ip == "1.1.1.1" { + foundCloudflare = true + } + } + assert.True(t, foundGoogle || foundCloudflare, + "R0011 must fire for 8.8.8.8 or 1.1.1.1, got: %v", ips) + }) + + // --------------------------------------------------------------- + // 28d. MITM — DNS spoofing simulation. + // fusioncore.ai is an allowed domain but the IP is spoofed. + // + // Step 1: nslookup fusioncore.ai (legitimate DNS, no alert). + // Step 2: curl --resolve fusioncore.ai:80:8.8.4.4 + // Simulates a DNS MITM returning a different IP. + // The domain is allowed but the connection goes to + // 8.8.4.4 (not 162.0.217.171) → R0011. + // --------------------------------------------------------------- + t.Run("mitm_spoofed_ip_R0011", func(t *testing.T) { + wl := setup(t) + + // Step 1: Legitimate DNS lookup — no alert expected. + wl.ExecIntoPod([]string{"nslookup", "fusioncore.ai"}, "curl") + + // Step 2: MITM — domain resolves to spoofed IP 8.8.4.4. + // curl --resolve skips DNS and connects directly to the + // spoofed IP, simulating what happens after DNS poisoning. + stdout, stderr, err := wl.ExecIntoPod( + []string{"curl", "-sm5", "--resolve", "fusioncore.ai:80:8.8.4.4", "http://fusioncore.ai"}, "curl") + t.Logf("curl MITM → err=%v stdout=%q stderr=%q", err, stdout, stderr) + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + t.Logf("=== R0011 alerts (%d) ===", len(r0011)) + logRichAlerts(t, r0011) + require.NotEmpty(t, r0011, "MITM: fusioncore.ai allowed but spoofed IP 8.8.4.4 must fire R0011") + + ips := testutils.LogAlertIPs(r0011) + t.Logf("R0011 anomalous IPs (MITM spoofed): %v", ips) + + found := false + for _, ip := range ips { + if ip == "8.8.4.4" { + found = true + break + } + } + assert.True(t, found, + "R0011 must specifically fire for spoofed IP 8.8.4.4, got: %v", ips) + }) + + // --------------------------------------------------------------- + // 28e. MITM — real CoreDNS poisoning via template plugin. + // Poisons CoreDNS so fusioncore.ai resolves to 8.8.4.4 + // instead of the legitimate 162.0.217.171. + // + // nslookup triggers the poisoned DNS response. + // R0005 does NOT fire: fusioncore.ai is in the NN egress + // list and BusyBox nslookup does NOT do PTR reverse-lookups. + // R0011 does NOT fire: no TCP egress (DNS is UDP to cluster + // DNS which is a private IP filtered by is_private_ip). + // + // This documents a detection gap: pure DNS MITM (without + // subsequent TCP to the spoofed IP) is invisible to both + // R0005 and R0011 when the domain is already whitelisted. + // + // NOTE: this subtest MUST run last — it modifies the + // cluster-wide CoreDNS configmap. + // --------------------------------------------------------------- + t.Run("mitm_coredns_poisoning", func(t *testing.T) { + wl := setup(t) + ctx := context.Background() + k8sClient := k8sinterface.NewKubernetesApi() + + // ── Back up original CoreDNS Corefile ── + cm, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + require.NoError(t, err, "get coredns configmap") + originalCorefile := cm.Data["Corefile"] + + restartAndWaitCoreDNS := func() { + deploy, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + require.NoError(t, err, "get coredns deployment") + if deploy.Spec.Template.ObjectMeta.Annotations == nil { + deploy.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + deploy.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + _, err = k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Update(ctx, deploy, metav1.UpdateOptions{}) + require.NoError(t, err, "restart coredns") + + require.Eventually(t, func() bool { + d, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil || d.Spec.Replicas == nil { + return false + } + return d.Status.ReadyReplicas == *d.Spec.Replicas && + d.Status.UpdatedReplicas == *d.Spec.Replicas + }, 60*time.Second, 2*time.Second, "coredns must become ready") + } + + // ── Restore CoreDNS on cleanup (best-effort) ── + t.Cleanup(func() { + t.Log("cleanup: restoring CoreDNS Corefile") + cm, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil { + t.Logf("cleanup: get coredns cm: %v", err) + return + } + cm.Data["Corefile"] = originalCorefile + if _, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Update(ctx, cm, metav1.UpdateOptions{}); err != nil { + t.Logf("cleanup: update coredns cm: %v", err) + return + } + deploy, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil { + t.Logf("cleanup: get coredns deploy: %v", err) + return + } + if deploy.Spec.Template.ObjectMeta.Annotations == nil { + deploy.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + deploy.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + if _, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Update(ctx, deploy, metav1.UpdateOptions{}); err != nil { + t.Logf("cleanup: restart coredns: %v", err) + } + }) + + // ── Poison CoreDNS: fusioncore.ai → 8.8.4.4 ── + poisoned := strings.Replace(originalCorefile, + "forward .", + "template IN A fusioncore.ai {\n answer \"fusioncore.ai. 60 IN A 8.8.4.4\"\n fallthrough\n }\n forward .", + 1) + require.NotEqual(t, originalCorefile, poisoned, "template injection must modify Corefile") + + cm.Data["Corefile"] = poisoned + _, err = k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Update(ctx, cm, metav1.UpdateOptions{}) + require.NoError(t, err, "apply poisoned Corefile") + restartAndWaitCoreDNS() + + // Verify poisoned DNS returns the spoofed IP. + require.Eventually(t, func() bool { + stdout, _, _ := wl.ExecIntoPod([]string{"nslookup", "fusioncore.ai"}, "curl") + return strings.Contains(stdout, "8.8.4.4") + }, 30*time.Second, 3*time.Second, "poisoned CoreDNS must return 8.8.4.4 for fusioncore.ai") + + // ── Trigger alerts ── + // nslookup does DNS only (no TCP egress). + // BusyBox nslookup does NOT do PTR reverse-lookups on result IPs. + stdout, stderr, err := wl.ExecIntoPod([]string{"nslookup", "fusioncore.ai"}, "curl") + t.Logf("nslookup (poisoned) → err=%v stdout=%q stderr=%q", err, stdout, stderr) + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + logRichAlerts(t, richAlerts) + + // R0005 does NOT fire: fusioncore.ai is already in the NN + // egress list, and BusyBox nslookup does NOT perform PTR + // reverse-lookups on result IPs, so no unknown domain is queried. + r0005 := testutils.FilterLogAlerts(richAlerts, "R0005") + if len(r0005) > 0 { + t.Logf("R0005 unexpected domains: %v", testutils.LogAlertDomains(r0005)) + } + assert.Empty(t, r0005, + "DNS MITM: domain is in NN and no PTR lookup — R0005 should not fire") + + // R0011 does NOT fire: nslookup generates only DNS (UDP) + // traffic to the cluster DNS service, which is a private IP + // excluded by is_private_ip(). + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + if len(r0011) > 0 { + t.Logf("R0011 unexpected IPs: %v", testutils.LogAlertIPs(r0011)) + } + assert.Empty(t, r0011, + "DNS MITM: nslookup has no TCP egress — R0011 should not fire") + }) + + // --------------------------------------------------------------- + // 28f. MITM — CoreDNS poisoning with TCP egress. + // Same CoreDNS poisoning as 28e, but now fusioncore.ai + // resolves to 128.130.194.56 (a routable IP that accepts + // TCP on port 80). curl generates a real TCP connection + // to the spoofed IP. + // + // Expected: + // R0005 = 0 — domain is in NN, no PTR reverse-lookup. + // R0011 fires — TCP egress to 128.130.194.56 which is + // NOT in the NN (NN only has 162.0.217.171). + // + // NOTE: runs after 28e; modifies cluster-wide CoreDNS. + // --------------------------------------------------------------- + t.Run("mitm_coredns_poisoning_tcp", func(t *testing.T) { + wl := setup(t) + ctx := context.Background() + k8sClient := k8sinterface.NewKubernetesApi() + + // ── Back up original CoreDNS Corefile ── + cm, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + require.NoError(t, err, "get coredns configmap") + originalCorefile := cm.Data["Corefile"] + + restartAndWaitCoreDNS := func() { + deploy, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + require.NoError(t, err, "get coredns deployment") + if deploy.Spec.Template.ObjectMeta.Annotations == nil { + deploy.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + deploy.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + _, err = k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Update(ctx, deploy, metav1.UpdateOptions{}) + require.NoError(t, err, "restart coredns") + + require.Eventually(t, func() bool { + d, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil || d.Spec.Replicas == nil { + return false + } + return d.Status.ReadyReplicas == *d.Spec.Replicas && + d.Status.UpdatedReplicas == *d.Spec.Replicas + }, 60*time.Second, 2*time.Second, "coredns must become ready") + } + + // ── Restore CoreDNS on cleanup (best-effort) ── + t.Cleanup(func() { + t.Log("cleanup: restoring CoreDNS Corefile") + cm, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil { + t.Logf("cleanup: get coredns cm: %v", err) + return + } + cm.Data["Corefile"] = originalCorefile + if _, err := k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Update(ctx, cm, metav1.UpdateOptions{}); err != nil { + t.Logf("cleanup: update coredns cm: %v", err) + return + } + deploy, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Get(ctx, "coredns", metav1.GetOptions{}) + if err != nil { + t.Logf("cleanup: get coredns deploy: %v", err) + return + } + if deploy.Spec.Template.ObjectMeta.Annotations == nil { + deploy.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + deploy.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + if _, err := k8sClient.KubernetesClient.AppsV1(). + Deployments("kube-system").Update(ctx, deploy, metav1.UpdateOptions{}); err != nil { + t.Logf("cleanup: restart coredns: %v", err) + } + }) + + // ── Poison CoreDNS: fusioncore.ai → 128.130.194.56 ── + poisoned := strings.Replace(originalCorefile, + "forward .", + "template IN A fusioncore.ai {\n answer \"fusioncore.ai. 60 IN A 128.130.194.56\"\n fallthrough\n }\n forward .", + 1) + require.NotEqual(t, originalCorefile, poisoned, "template injection must modify Corefile") + + cm.Data["Corefile"] = poisoned + _, err = k8sClient.KubernetesClient.CoreV1(). + ConfigMaps("kube-system").Update(ctx, cm, metav1.UpdateOptions{}) + require.NoError(t, err, "apply poisoned Corefile") + restartAndWaitCoreDNS() + + // Verify poisoned DNS returns the spoofed IP. + require.Eventually(t, func() bool { + stdout, _, _ := wl.ExecIntoPod([]string{"nslookup", "fusioncore.ai"}, "curl") + return strings.Contains(stdout, "128.130.194.56") + }, 30*time.Second, 3*time.Second, "poisoned CoreDNS must return 128.130.194.56 for fusioncore.ai") + + // ── Trigger alerts ── + // curl resolves fusioncore.ai → 128.130.194.56 (poisoned) + // then opens a TCP connection to 128.130.194.56:80. + stdout, stderr, err := wl.ExecIntoPod( + []string{"curl", "-sm5", "http://fusioncore.ai"}, "curl") + t.Logf("curl (poisoned DNS) → err=%v stdout=%q stderr=%q", err, stdout, stderr) + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + + // R0005 does NOT fire: fusioncore.ai is already in the NN + // egress list, and curl (like BusyBox nslookup) does NOT + // perform PTR reverse-lookups on resolved IPs. + r0005 := testutils.FilterLogAlerts(richAlerts, "R0005") + if len(r0005) > 0 { + t.Logf("R0005 unexpected domains: %v", testutils.LogAlertDomains(r0005)) + } + assert.Empty(t, r0005, + "DNS MITM: domain is in NN and no PTR lookup — R0005 should not fire") + + // R0011 fires: TCP egress to 128.130.194.56 which is NOT + // in the NN (NN only allows 162.0.217.171). + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + t.Logf("=== R0011 alerts (%d) ===", len(r0011)) + logRichAlerts(t, r0011) + require.NotEmpty(t, r0011, + "DNS MITM: TCP to spoofed IP 128.130.194.56 must fire R0011") + + ips := testutils.LogAlertIPs(r0011) + t.Logf("R0011 anomalous IPs (poisoned DNS TCP): %v", ips) + + found := false + for _, ip := range ips { + if ip == "128.130.194.56" { + found = true + break + } + } + assert.True(t, found, + "R0011 must specifically fire for poisoned IP 128.130.194.56, got: %v", ips) + }) + + // --------------------------------------------------------------- + // 28g. Service discovery via PTR sweep. + // An attacker inside a pod sweeps the Kubernetes service + // CIDR with reverse DNS lookups (PTR queries) to discover + // service names. Each nslookup generates a PTR query + // for .in-addr.arpa. which is NOT in the NN. + // + // Expected: + // R0005 > 0 — PTR queries are not in NN egress. + // R0011 = 0 — DNS traffic stays on the private cluster + // DNS IP, filtered by is_private_ip(). + // + // Requires: R0005 without the .svc.cluster.local. exclusion + // (the NN is the sole whitelist). + // --------------------------------------------------------------- + t.Run("ptr_sweep_service_discovery", func(t *testing.T) { + wl := setup(t) + k8sClient := k8sinterface.NewKubernetesApi() + + // Discover the service CIDR base from the kubernetes service IP. + kubeSvc, err := k8sClient.KubernetesClient.CoreV1(). + Services("default").Get(context.Background(), "kubernetes", metav1.GetOptions{}) + require.NoError(t, err, "get kubernetes service") + clusterIP := kubeSvc.Spec.ClusterIP + t.Logf("kubernetes service clusterIP: %s", clusterIP) + + // Extract the /24 base (e.g. "10.96.0" from "10.96.0.1"). + parts := strings.Split(clusterIP, ".") + require.Len(t, parts, 4, "clusterIP must be IPv4") + base := strings.Join(parts[:3], ".") + + // Build a sweep command: reverse-lookup all 254 IPs in the service CIDR. + // BusyBox nslookup does a PTR query when given an IP address. + // Each PTR query generates event.name = ".in-addr.arpa." + // which is NOT in the NN → R0005 fires. + // + // We batch into groups of 50 to avoid command-line length limits + // and exec timeouts. + batchSize := 50 + for batchStart := 1; batchStart <= 254; batchStart += batchSize { + batchEnd := batchStart + batchSize - 1 + if batchEnd > 254 { + batchEnd = 254 + } + var sweepCmd strings.Builder + for i := batchStart; i <= batchEnd; i++ { + if i > batchStart { + sweepCmd.WriteString("; ") + } + sweepCmd.WriteString(fmt.Sprintf("nslookup %s.%d 2>/dev/null || true", base, i)) + } + stdout, stderr, err := wl.ExecIntoPod( + []string{"sh", "-c", sweepCmd.String()}, "curl") + t.Logf("PTR sweep [%d-%d] → err=%v stdout_len=%d stderr_len=%d", + batchStart, batchEnd, err, len(stdout), len(stderr)) + } + + alerts := waitAlerts(t, wl.Namespace) + t.Logf("=== %d AlertManager alerts ===", len(alerts)) + logAlerts(t, alerts) + + richAlerts := getLogAlerts(t, wl.Namespace) + + // R0005 fires: PTR queries like "1.0.96.10.in-addr.arpa." are + // not in the NN egress. Multiple should fire (one per unique domain). + r0005 := testutils.FilterLogAlerts(richAlerts, "R0005") + t.Logf("=== R0005 alerts (%d) ===", len(r0005)) + logRichAlerts(t, r0005) + require.NotEmpty(t, r0005, + "PTR sweep: reverse DNS queries must trigger R0005") + + domains := testutils.LogAlertDomains(r0005) + t.Logf("R0005 anomalous domains (%d unique): %v", len(domains), domains) + + // Verify the alerts contain in-addr.arpa PTR domains. + ptrCount := 0 + for _, d := range domains { + if strings.Contains(d, "in-addr.arpa") { + ptrCount++ + } + } + t.Logf("R0005 PTR domains (in-addr.arpa): %d out of %d", ptrCount, len(domains)) + assert.Greater(t, ptrCount, 0, + "R0005 must contain in-addr.arpa PTR domains from the sweep") + + // With 254 lookups we expect a meaningful number of R0005 alerts. + // Exact count depends on dedup, but should be more than 1. + assert.Greater(t, len(r0005), 1, + "PTR sweep of 254 IPs should produce multiple R0005 alerts, got %d", len(r0005)) + + // R0011 does NOT fire: all DNS traffic goes to the cluster DNS + // service (private IP), which is filtered by is_private_ip(). + r0011 := testutils.FilterLogAlerts(richAlerts, "R0011") + if len(r0011) > 0 { + t.Logf("R0011 unexpected IPs: %v", testutils.LogAlertIPs(r0011)) + } + assert.Empty(t, r0011, + "PTR sweep: no egress to public IPs — R0011 should not fire") + }) +} + +// Test_29_SignedApplicationProfile verifies that a cryptographically signed +// ApplicationProfile can be pushed to storage, loaded by node-agent, and +// used for anomaly detection just like any other user-defined profile. +// +// The test signs an AP with key-based ECDSA (no OIDC/Sigstore needed), +// pushes it to storage, verifies the signature survives the round-trip, +// deploys a pod referencing the signed profile, and asserts that executing +// a binary NOT in the profile fires R0001 (Unexpected process launched). +func Test_29_SignedApplicationProfile(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + ns := testutils.NewRandomNamespace() + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + + // ── 1. Build the ApplicationProfile ── + // Use nil (not empty slices) for unused fields — storage normalizes + // []string{} → nil on save, which changes the content hash. + // Matching the storage representation ensures the signature survives + // the round-trip (same approach as cluster_flow_test.go). + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "signed-ap", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep"}, + {Path: "/usr/bin/curl"}, + }, + Syscalls: []string{"socket", "connect", "read", "write", "close", "openat"}, + }, + }, + }, + } + + // ── 2. Sign the AP (key-based, no OIDC) ── + adapter := profiles.NewApplicationProfileAdapter(ap) + err := signature.SignObjectDisableKeyless(adapter) + require.NoError(t, err, "sign AP") + require.True(t, signature.IsSigned(adapter), "AP must be signed") + + // Verify signature locally. + require.NoError(t, signature.VerifyObjectAllowUntrusted(adapter), + "signature must verify immediately after signing") + + sig, err := signature.GetObjectSignature(adapter) + require.NoError(t, err, "extract signature") + require.NotEmpty(t, sig.Signature, "signature bytes must not be empty") + require.NotEmpty(t, sig.Certificate, "certificate must not be empty") + t.Logf("AP signed: issuer=%s identity=%s sigLen=%d", sig.Issuer, sig.Identity, len(sig.Signature)) + + // ── 3. Push signed AP to storage ── + // Create preserves annotations (including signature.*). + _, err = storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), ap, metav1.CreateOptions{}) + require.NoError(t, err, "create signed AP in storage") + + // ── 4. Verify signature survives the storage round-trip ── + require.Eventually(t, func() bool { + stored, getErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), "signed-ap", v1.GetOptions{}) + if getErr != nil { + return false + } + return signature.IsSigned(profiles.NewApplicationProfileAdapter(stored)) + }, 30*time.Second, 1*time.Second, "stored AP must retain signature annotations") + + storedAP, err := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), "signed-ap", v1.GetOptions{}) + require.NoError(t, err) + storedAdapter := profiles.NewApplicationProfileAdapter(storedAP) + err = signature.VerifyObjectAllowUntrusted(storedAdapter) + require.NoError(t, err, "stored AP signature must still verify after round-trip") + t.Log("Signature round-trip verification passed") + + // ── 6. Deploy pod referencing the signed profile ── + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/curl-signed-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + time.Sleep(15 * time.Second) // let node-agent load the profile + + // ── 7. Exec an allowed binary — should NOT fire R0001 ── + stdout, stderr, execErr := wl.ExecIntoPod([]string{"curl", "-sm5", "http://ebpf.io"}, "curl") + t.Logf("curl (allowed) → err=%v stdout=%q stderr=%q", execErr, stdout, stderr) + + // ── 8. Exec an anomalous binary — should fire R0001 ── + // The user-defined profile may not be cached yet when the first exec runs. + // Re-exec nslookup on each poll so the eBPF event is generated after + // the profile is loaded (same race as the crypto miner test). + stdout, stderr, execErr = wl.ExecIntoPod([]string{"nslookup", "ebpf.io"}, "curl") + t.Logf("nslookup (anomalous) → err=%v stdout=%q stderr=%q", execErr, stdout, stderr) + + // ── 9. Wait for R0001 alert ── + var alerts []testutils.Alert + require.Eventually(t, func() bool { + // Re-exec on each poll to ensure the event arrives after the profile is cached. + wl.ExecIntoPod([]string{"nslookup", "ebpf.io"}, "curl") + + alerts, err = testutils.GetAlerts(ns.Name) + if err != nil || len(alerts) == 0 { + return false + } + for _, a := range alerts { + if a.Labels["rule_id"] == "R0001" { + return true + } + } + return false + }, 120*time.Second, 10*time.Second, "nslookup is not in signed AP — must fire R0001") + + // Extra settle time. + time.Sleep(10 * time.Second) + alerts, _ = testutils.GetAlerts(ns.Name) + + t.Logf("=== %d alerts ===", len(alerts)) + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + + // R0001 must have fired for the anomalous exec. + r0001Count := 0 + for _, a := range alerts { + if a.Labels["rule_id"] == "R0001" { + r0001Count++ + } + } + require.Greater(t, r0001Count, 0, "nslookup not in signed AP must fire R0001") +} + +// Test_30_TamperedSignedProfiles verifies that cryptographic signature +// verification detects tampering of both ApplicationProfile and +// NetworkNeighborhood objects. +// +// Current state of enforcement (as of merge): +// - enableSignatureVerification defaults to false +// - When enabled: tampered profiles are silently SKIPPED (not loaded) +// - No R-number rule fires on signature verification failure +// - User-defined NNs in addContainer() are NOT verified (known gap) +// - System fails open: no profile → no anomaly baseline → no detection +// +// This test proves: +// - The crypto layer detects tampering (sign → tamper → verify fails) +// - Without enforcement, tampered profiles are loaded and used +func Test_30_TamperedSignedProfiles(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + // --------------------------------------------------------------- + // 30a. Tamper detection at the crypto layer — AP and NN. + // Sign both objects, tamper their specs, verify fails. + // --------------------------------------------------------------- + t.Run("tamper_invalidates_signature", func(t *testing.T) { + // ── ApplicationProfile ── + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tamper-test-ap", + Namespace: "tamper-test-ns", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "app", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep"}, + {Path: "/usr/bin/curl"}, + }, + Syscalls: []string{"read", "write", "close"}, + }, + }, + }, + } + + apAdapter := profiles.NewApplicationProfileAdapter(ap) + require.NoError(t, signature.SignObjectDisableKeyless(apAdapter), "sign AP") + require.True(t, signature.IsSigned(apAdapter)) + require.NoError(t, signature.VerifyObjectAllowUntrusted(apAdapter), "untampered AP must verify") + + // Tamper: attacker adds nslookup to whitelist + ap.Spec.Containers[0].Execs = append(ap.Spec.Containers[0].Execs, + v1beta1.ExecCalls{Path: "/usr/bin/nslookup"}) + + tamperedAPAdapter := profiles.NewApplicationProfileAdapter(ap) + err := signature.VerifyObjectAllowUntrusted(tamperedAPAdapter) + require.Error(t, err, "tampered AP must fail verification") + t.Logf("AP tamper detected: %v", err) + + // ── NetworkNeighborhood ── + nn := &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tamper-test-nn", + Namespace: "tamper-test-ns", + Annotations: map[string]string{ + helpersv1.ManagedByMetadataKey: helpersv1.ManagedByUserValue, + helpersv1.StatusMetadataKey: helpersv1.Completed, + helpersv1.CompletionMetadataKey: helpersv1.Full, + }, + Labels: map[string]string{ + helpersv1.KindMetadataKey: "Deployment", + helpersv1.NameMetadataKey: "tamper-test", + }, + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "tamper-test"}, + }, + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "app", + Egress: []v1beta1.NetworkNeighbor{ + { + Identifier: "allowed-egress", + Type: "external", + DNS: "fusioncore.ai.", + DNSNames: []string{"fusioncore.ai."}, + IPAddress: "162.0.217.171", + Ports: []v1beta1.NetworkPort{ + {Name: "TCP-80", Protocol: "TCP", Port: ptr.To(int32(80))}, + }, + }, + }, + }, + }, + }, + } + + nnAdapter := profiles.NewNetworkNeighborhoodAdapter(nn) + require.NoError(t, signature.SignObjectDisableKeyless(nnAdapter), "sign NN") + require.True(t, signature.IsSigned(nnAdapter)) + require.NoError(t, signature.VerifyObjectAllowUntrusted(nnAdapter), "untampered NN must verify") + + // Tamper: attacker adds a C2 domain to the egress whitelist + nn.Spec.Containers[0].Egress = append(nn.Spec.Containers[0].Egress, + v1beta1.NetworkNeighbor{ + Identifier: "c2-backdoor", + Type: "external", + DNS: "evil-c2.example.com.", + DNSNames: []string{"evil-c2.example.com."}, + IPAddress: "6.6.6.6", + Ports: []v1beta1.NetworkPort{ + {Name: "TCP-443", Protocol: "TCP", Port: ptr.To(int32(443))}, + }, + }) + + tamperedNNAdapter := profiles.NewNetworkNeighborhoodAdapter(nn) + err = signature.VerifyObjectAllowUntrusted(tamperedNNAdapter) + require.Error(t, err, "tampered NN must fail verification") + t.Logf("NN tamper detected: %v", err) + }) + + // --------------------------------------------------------------- + // 30b. Tampered AP is still loaded when enforcement is off. + // + // enableSignatureVerification defaults to false. + // The tampered profile is pushed to storage and node-agent + // loads it without checking the signature. Anomaly detection + // uses the tampered baseline → the attacker's added exec + // path (nslookup) is whitelisted. + // + // With enableSignatureVerification=true, the tampered profile + // would be rejected and the pod would have no baseline. + // --------------------------------------------------------------- + t.Run("tampered_profile_loaded_without_enforcement", func(t *testing.T) { + ns := testutils.NewRandomNamespace() + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + + // Build AP: only sleep + curl allowed. + // Use nil for unused fields (storage normalizes empty slices to nil). + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "signed-ap", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep"}, + {Path: "/usr/bin/curl"}, + }, + Syscalls: []string{"socket", "connect", "read", "write", "close", "openat"}, + }, + }, + }, + } + + // Sign the AP. + apAdapter := profiles.NewApplicationProfileAdapter(ap) + require.NoError(t, signature.SignObjectDisableKeyless(apAdapter)) + require.NoError(t, signature.VerifyObjectAllowUntrusted(apAdapter), "pre-tamper verification") + + // Tamper: attacker adds nslookup to the whitelist. + ap.Spec.Containers[0].Execs = append(ap.Spec.Containers[0].Execs, + v1beta1.ExecCalls{Path: "/usr/bin/nslookup"}) + + // Signature is now invalid. + tamperedAdapter := profiles.NewApplicationProfileAdapter(ap) + require.Error(t, signature.VerifyObjectAllowUntrusted(tamperedAdapter), + "tampered AP must fail verification") + + // Push tampered AP to storage (signature annotations are stale). + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), ap, metav1.CreateOptions{}) + require.NoError(t, err, "push tampered AP to storage") + + // Verify stored AP has stale signature. + require.Eventually(t, func() bool { + stored, getErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), "signed-ap", v1.GetOptions{}) + if getErr != nil { + return false + } + storedAdapter := profiles.NewApplicationProfileAdapter(stored) + // Signature annotation exists but verification should fail. + if !signature.IsSigned(storedAdapter) { + return false + } + return signature.VerifyObjectAllowUntrusted(storedAdapter) != nil + }, 30*time.Second, 1*time.Second, "stored AP must have stale signature that fails verification") + t.Log("Stored AP has invalid signature (tamper detected at crypto layer)") + + // Deploy pod referencing the tampered profile. + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/curl-signed-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + time.Sleep(15 * time.Second) // let node-agent load profiles + + // Execute nslookup — the attacker added this to the whitelist. + // With enforcement OFF: profile is loaded despite invalid signature, + // so nslookup is "allowed" and R0001 should NOT fire for it. + wl.ExecIntoPod([]string{"nslookup", "ebpf.io"}, "curl") + + // Execute wget — NOT in the AP (even after tampering). + wl.ExecIntoPod([]string{"wget", "-qO-", "--timeout=5", "http://ebpf.io"}, "curl") + + // Wait for alerts. + var alerts []testutils.Alert + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(ns.Name) + if err != nil || len(alerts) == 0 { + return false + } + for _, a := range alerts { + if a.Labels["rule_id"] == "R0001" { + return true + } + } + return false + }, 60*time.Second, 5*time.Second, "wget not in tampered AP must fire R0001") + + time.Sleep(10 * time.Second) + alerts, _ = testutils.GetAlerts(ns.Name) + + t.Logf("=== %d alerts ===", len(alerts)) + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + + // R0001 must have fired (tampered profile was loaded and used). + r0001Count := 0 + for _, a := range alerts { + if a.Labels["rule_id"] == "R0001" { + r0001Count++ + } + } + require.Greater(t, r0001Count, 0, + "R0001 must fire — proves tampered profile was loaded (enableSignatureVerification=false)") + + // No dedicated tamper-detection alert exists (no R-number for this). + // With enableSignatureVerification=true: + // - The tampered AP would be rejected (verifyApplicationProfile returns error) + // - ProfileState.Status would be set to "verification-failed" + // - The pod would have no baseline → no anomaly rules fire + // - System fails OPEN (attacker evades detection by tampering the profile) + // - NOTE: user-defined NNs in addContainer() are NOT verified (known gap) + t.Log("NOTE: No tamper-detection alert rule exists. With enableSignatureVerification=true,") + t.Log(" the tampered profile would be silently rejected. No R-number fires for tampering.") + }) +} + +// Test_31_TamperDetectionAlert verifies that R1016 fires when a previously +// signed ApplicationProfile has been tampered with (signature is stale). +// +// This test proves the new tamper-detection alerting: +// - Sign an AP, push to storage +// - Tamper the AP in storage (modify spec, keep stale signature annotations) +// - Deploy a pod referencing the tampered profile +// - R1016 "Signed profile tampered" must fire +// +// R1016 fires regardless of enableSignatureVerification setting. +// The detection happens in the AP cache when it loads the profile. +func Test_31_TamperDetectionAlert(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + ns := testutils.NewRandomNamespace() + k8sClient := k8sinterface.NewKubernetesApi() + storageClient := spdxv1beta1client.NewForConfigOrDie(k8sClient.K8SConfig) + + // ── 1. Build and sign an ApplicationProfile ── + ap := &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "signed-ap", + Namespace: ns.Name, + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "curl", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/sleep"}, + {Path: "/usr/bin/curl"}, + }, + Syscalls: []string{"socket", "connect", "read", "write", "close", "openat"}, + }, + }, + }, + } + + apAdapter := profiles.NewApplicationProfileAdapter(ap) + require.NoError(t, signature.SignObjectDisableKeyless(apAdapter), "sign AP") + require.True(t, signature.IsSigned(apAdapter), "AP must be signed") + require.NoError(t, signature.VerifyObjectAllowUntrusted(apAdapter), + "signature must verify immediately after signing") + t.Log("AP signed successfully") + + // ── 2. Tamper the AP (add unauthorized exec path) ── + ap.Spec.Containers[0].Execs = append(ap.Spec.Containers[0].Execs, + v1beta1.ExecCalls{Path: "/usr/bin/nslookup"}) + + // Verify the signature is now invalid + tamperedAdapter := profiles.NewApplicationProfileAdapter(ap) + require.Error(t, signature.VerifyObjectAllowUntrusted(tamperedAdapter), + "tampered AP must fail verification") + require.True(t, signature.IsSigned(tamperedAdapter), + "tampered AP must still have signature annotations (stale)") + t.Log("AP tampered — signature is stale") + + // ── 3. Push tampered AP to storage ── + _, err := storageClient.ApplicationProfiles(ns.Name).Create( + context.Background(), ap, metav1.CreateOptions{}) + require.NoError(t, err, "push tampered AP to storage") + + // Verify it's stored with stale signature + require.Eventually(t, func() bool { + stored, getErr := storageClient.ApplicationProfiles(ns.Name).Get( + context.Background(), "signed-ap", v1.GetOptions{}) + if getErr != nil { + return false + } + storedAdapter := profiles.NewApplicationProfileAdapter(stored) + return signature.IsSigned(storedAdapter) && + signature.VerifyObjectAllowUntrusted(storedAdapter) != nil + }, 30*time.Second, 1*time.Second, "stored AP must have stale signature") + t.Log("Tampered AP stored with stale signature") + + // ── 4. Deploy pod referencing the tampered profile ── + wl, err := testutils.NewTestWorkload(ns.Name, + path.Join(utils.CurrentDir(), "resources/curl-signed-deployment.yaml")) + require.NoError(t, err) + require.NoError(t, wl.WaitForReady(80)) + t.Log("Pod deployed, waiting for cache to detect tamper...") + + // ── 5. Wait for R1016 "Signed profile tampered" alert ── + // The AP cache's addContainer or periodicUpdate will detect the tampered + // signature and emit R1016 via the exporter. + var alerts []testutils.Alert + require.Eventually(t, func() bool { + alerts, err = testutils.GetAlerts(ns.Name) + if err != nil || len(alerts) == 0 { + return false + } + for _, a := range alerts { + if a.Labels["rule_id"] == "R1016" { + return true + } + } + return false + }, 120*time.Second, 5*time.Second, "R1016 must fire for tampered signed AP") + + // ── 6. Log all alerts for debugging ── + time.Sleep(5 * time.Second) + alerts, _ = testutils.GetAlerts(ns.Name) + + t.Logf("=== %d alerts ===", len(alerts)) + for i, a := range alerts { + t.Logf(" [%d] %s(%s) comm=%s container=%s", + i, a.Labels["rule_name"], a.Labels["rule_id"], + a.Labels["comm"], a.Labels["container_name"]) + } + + // Verify R1016 alert details + r1016Count := 0 + for _, a := range alerts { + if a.Labels["rule_id"] == "R1016" { + r1016Count++ + assert.Equal(t, "Signed profile tampered", a.Labels["rule_name"], + "R1016 alert must have correct rule name") + assert.Equal(t, ns.Name, a.Labels["namespace"], + "R1016 alert must have correct namespace") + t.Logf("R1016 alert: rule_name=%s namespace=%s severity=%s", + a.Labels["rule_name"], a.Labels["namespace"], a.Labels["severity"]) + } + } + require.Greater(t, r1016Count, 0, + "R1016 must fire — proves tamper detection alerting works") + t.Log("Tamper detection alerting verified successfully") +} + +func Test_32_CollapseConfigurationCRD(t *testing.T) { + k8sClient := k8sinterface.NewKubernetesApi() + dynClient, err := dynamic.NewForConfig(k8sClient.K8SConfig) + require.NoError(t, err, "create dynamic client") + + collapseGVR := schema.GroupVersionResource{ + Group: "spdx.softwarecomposition.kubescape.io", + Version: "v1beta1", + Resource: "collapseconfigurations", + } + ctx := context.Background() + + t.Run("CRUD", func(t *testing.T) { + name := fmt.Sprintf("test-collapse-%d", time.Now().UnixNano()%10000) + defer func() { + _ = dynClient.Resource(collapseGVR).Delete(ctx, name, metav1.DeleteOptions{}) + }() + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "CollapseConfiguration", + "metadata": map[string]interface{}{"name": name}, + "spec": map[string]interface{}{ + "openDynamicThreshold": 50, + "endpointDynamicThreshold": 100, + "collapseConfigs": []interface{}{ + map[string]interface{}{"prefix": "/etc", "threshold": 10}, + map[string]interface{}{"prefix": "/var/log", "threshold": 20}, + }, + }, + }, + } + + _, err := dynClient.Resource(collapseGVR).Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err, "create CollapseConfiguration") + + got, err := dynClient.Resource(collapseGVR).Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "get CollapseConfiguration") + spec := got.Object["spec"].(map[string]interface{}) + assert.EqualValues(t, 50, spec["openDynamicThreshold"]) + assert.EqualValues(t, 100, spec["endpointDynamicThreshold"]) + + list, err := dynClient.Resource(collapseGVR).List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "list") + found := false + for _, item := range list.Items { + if item.GetName() == name { + found = true + } + } + require.True(t, found) + + got.Object["spec"].(map[string]interface{})["openDynamicThreshold"] = int64(75) + _, err = dynClient.Resource(collapseGVR).Update(ctx, got, metav1.UpdateOptions{}) + require.NoError(t, err, "update") + got2, err := dynClient.Resource(collapseGVR).Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err) + assert.EqualValues(t, 75, got2.Object["spec"].(map[string]interface{})["openDynamicThreshold"]) + + require.NoError(t, dynClient.Resource(collapseGVR).Delete(ctx, name, metav1.DeleteOptions{})) + _, err = dynClient.Resource(collapseGVR).Get(ctx, name, metav1.GetOptions{}) + require.Error(t, err) + t.Log("CRUD verified") + }) + + t.Run("CollapseAffectsLearnedProfile", func(t *testing.T) { + // ── 1. Create CollapseConfiguration with aggressive thresholds ── + // Nginx opens many files under /etc and /lib; with low thresholds these should collapse. + ccName := "default" // storage looks for "default" first + defer func() { + _ = dynClient.Resource(collapseGVR).Delete(ctx, ccName, metav1.DeleteOptions{}) + }() + + cc := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "CollapseConfiguration", + "metadata": map[string]interface{}{"name": ccName}, + "spec": map[string]interface{}{ + "openDynamicThreshold": int64(50), + "endpointDynamicThreshold": int64(100), + "collapseConfigs": []interface{}{ + map[string]interface{}{"prefix": "/etc", "threshold": int64(3)}, + map[string]interface{}{"prefix": "/usr", "threshold": int64(5)}, + map[string]interface{}{"prefix": "/usr/bin", "threshold": int64(3)}, + map[string]interface{}{"prefix": "/lib", "threshold": int64(5)}, + }, + }, + }, + } + _, err := dynClient.Resource(collapseGVR).Create(ctx, cc, metav1.CreateOptions{}) + require.NoError(t, err, "create CollapseConfiguration") + t.Log("Created CollapseConfiguration 'default' with aggressive thresholds") + + // Verify it was stored + got, err := dynClient.Resource(collapseGVR).Get(ctx, ccName, metav1.GetOptions{}) + require.NoError(t, err) + t.Logf("CollapseConfig spec: %v", got.Object["spec"]) + + // ── 2. Deploy nginx and wait for AP to complete ── + ns := testutils.NewRandomNamespace() + t.Logf("Using namespace %s", ns.Name) + wl, err := testutils.NewTestWorkload(ns.Name, path.Join(utils.CurrentDir(), "resources/nginx-deployment.yaml")) + require.NoError(t, err, "create nginx workload") + require.NoError(t, wl.WaitForReady(30), "wait for nginx ready") + + // Give it a moment to generate file opens + time.Sleep(10 * time.Second) + + // ── 3. Wait for AP completion ── + err = wl.WaitForApplicationProfileCompletion(30) // 30 retries × 10s = 5 min max + require.NoError(t, err, "wait for AP completion") + + // ── 4. Get the AP and inspect Opens ── + ap, err := wl.GetApplicationProfile() + require.NoError(t, err, "get application profile") + require.NotEmpty(t, ap.Spec.Containers, "AP must have containers") + + for _, container := range ap.Spec.Containers { + t.Logf("Container %q: %d opens", container.Name, len(container.Opens)) + var collapsedEtc, collapsedLib, collapsedUsr bool + for _, o := range container.Opens { + t.Logf(" open: %s (flags: %v)", o.Path, o.Flags) + // Check for collapse markers (⋯ or *) in paths + if (strings.HasPrefix(o.Path, "/etc/") && strings.Contains(o.Path, "⋯")) || + (strings.HasPrefix(o.Path, "/etc/") && strings.Contains(o.Path, "*")) { + collapsedEtc = true + } + if (strings.HasPrefix(o.Path, "/lib") && strings.Contains(o.Path, "⋯")) || + (strings.HasPrefix(o.Path, "/lib") && strings.Contains(o.Path, "*")) { + collapsedLib = true + } + if (strings.HasPrefix(o.Path, "/usr/") && strings.Contains(o.Path, "⋯")) || + (strings.HasPrefix(o.Path, "/usr/") && strings.Contains(o.Path, "*")) { + collapsedUsr = true + } + } + // With threshold=3 for /etc, nginx should collapse (it opens >3 files under /etc) + t.Logf("Collapse detected: /etc=%v /lib=%v /usr=%v", collapsedEtc, collapsedLib, collapsedUsr) + assert.True(t, collapsedEtc, "expected /etc paths to be collapsed with threshold=3") + } + t.Log("Phase 1 verified — CollapseConfiguration affects AP collapsing") + + // ── 5. Update CollapseConfiguration with higher thresholds ── + got, err = dynClient.Resource(collapseGVR).Get(ctx, ccName, metav1.GetOptions{}) + require.NoError(t, err) + got.Object["spec"] = map[string]interface{}{ + "openDynamicThreshold": int64(50), + "endpointDynamicThreshold": int64(100), + "collapseConfigs": []interface{}{ + map[string]interface{}{"prefix": "/etc", "threshold": int64(500)}, + map[string]interface{}{"prefix": "/usr", "threshold": int64(500)}, + map[string]interface{}{"prefix": "/lib", "threshold": int64(500)}, + }, + } + _, err = dynClient.Resource(collapseGVR).Update(ctx, got, metav1.UpdateOptions{}) + require.NoError(t, err, "update CollapseConfig with high thresholds") + t.Log("Updated CollapseConfiguration — thresholds raised to 500") + + // ── 6. Deploy nginx in a fresh namespace ── + ns2 := testutils.NewRandomNamespace() + t.Logf("Using namespace %s for phase 2", ns2.Name) + wl2, err := testutils.NewTestWorkload(ns2.Name, path.Join(utils.CurrentDir(), "resources/nginx-deployment.yaml")) + require.NoError(t, err, "create nginx workload (phase 2)") + require.NoError(t, wl2.WaitForReady(30), "wait for nginx ready (phase 2)") + + time.Sleep(10 * time.Second) + + err = wl2.WaitForApplicationProfileCompletion(30) + require.NoError(t, err, "wait for AP completion (phase 2)") + + // ── 7. Verify paths are NOT collapsed with high thresholds ── + ap2, err := wl2.GetApplicationProfile() + require.NoError(t, err, "get AP (phase 2)") + require.NotEmpty(t, ap2.Spec.Containers) + + for _, container := range ap2.Spec.Containers { + t.Logf("Phase 2 — Container %q: %d opens", container.Name, len(container.Opens)) + var collapsedEtc2 bool + for _, o := range container.Opens { + t.Logf(" open: %s (flags: %v)", o.Path, o.Flags) + if strings.HasPrefix(o.Path, "/etc/") && + (strings.Contains(o.Path, "⋯") || strings.Contains(o.Path, "*")) { + collapsedEtc2 = true + } + } + // With threshold=500, nginx should NOT collapse /etc (it opens far fewer than 500 files) + t.Logf("Phase 2 collapse detected: /etc=%v", collapsedEtc2) + assert.False(t, collapsedEtc2, "expected /etc paths NOT collapsed with threshold=500") + } + t.Log("Phase 2 verified — higher thresholds prevent collapsing") + }) + + t.Run("ConsolidationRemovesSubsumedPaths", func(t *testing.T) { + // After collapsing, individual paths that are subsumed by a wildcard/dynamic + // pattern (e.g. /etc/⋯) should be removed from the profile. This test + // verifies that consolidateOpens works end-to-end. + ccName := "default" + defer func() { + _ = dynClient.Resource(collapseGVR).Delete(ctx, ccName, metav1.DeleteOptions{}) + }() + + // Very low threshold for /etc to trigger collapse + cc := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "CollapseConfiguration", + "metadata": map[string]interface{}{"name": ccName}, + "spec": map[string]interface{}{ + "openDynamicThreshold": int64(50), + "endpointDynamicThreshold": int64(100), + "collapseConfigs": []interface{}{ + map[string]interface{}{"prefix": "/etc", "threshold": int64(3)}, + map[string]interface{}{"prefix": "/lib", "threshold": int64(3)}, + map[string]interface{}{"prefix": "/usr", "threshold": int64(3)}, + }, + }, + }, + } + _, err := dynClient.Resource(collapseGVR).Create(ctx, cc, metav1.CreateOptions{}) + require.NoError(t, err, "create CollapseConfiguration") + t.Log("Created CollapseConfiguration with threshold=3 for /etc, /lib, /usr") + + // Deploy nginx + ns := testutils.NewRandomNamespace() + t.Logf("Using namespace %s", ns.Name) + wl, err := testutils.NewTestWorkload(ns.Name, path.Join(utils.CurrentDir(), "resources/nginx-deployment.yaml")) + require.NoError(t, err, "create nginx workload") + require.NoError(t, wl.WaitForReady(30), "wait for nginx ready") + + time.Sleep(10 * time.Second) + + err = wl.WaitForApplicationProfileCompletion(30) + require.NoError(t, err, "wait for AP completion") + + ap, err := wl.GetApplicationProfile() + require.NoError(t, err, "get application profile") + require.NotEmpty(t, ap.Spec.Containers, "AP must have containers") + + for _, container := range ap.Spec.Containers { + t.Logf("Container %q: %d opens", container.Name, len(container.Opens)) + + hasDynamicEtc := false + singleSegmentEtcPaths := 0 // paths like /etc/hosts, /etc/passwd (no ⋯ or *) + + for _, o := range container.Opens { + t.Logf(" open: %s (flags: %v)", o.Path, o.Flags) + + if o.Path == "/etc/\u22ef" || o.Path == "/etc/*" { + hasDynamicEtc = true + } + + // Count individual /etc/X paths (single segment after /etc/, no wildcards) + if strings.HasPrefix(o.Path, "/etc/") && + !strings.Contains(o.Path, "\u22ef") && + !strings.Contains(o.Path, "*") { + parts := strings.Split(strings.TrimPrefix(o.Path, "/etc/"), "/") + if len(parts) == 1 { + singleSegmentEtcPaths++ + t.Logf(" *** single-segment /etc path NOT consolidated: %s", o.Path) + } + } + } + + // With threshold=3 and nginx opening many /etc files, we expect /etc/⋯ + assert.True(t, hasDynamicEtc, "expected /etc/⋯ pattern from collapse") + + // The consolidation should have removed individual single-segment /etc paths + // because they are subsumed by /etc/⋯ + assert.Equal(t, 0, singleSegmentEtcPaths, + "individual single-segment /etc/ paths should be consolidated away by /etc/⋯") + } + t.Log("Consolidation verified — subsumed paths removed from profile") + }) + + t.Run("StressTest200Entries", func(t *testing.T) { + // Create a CollapseConfiguration with 200 entries at various depths + ccName := "default" + defer func() { + _ = dynClient.Resource(collapseGVR).Delete(ctx, ccName, metav1.DeleteOptions{}) + }() + + // Generate 200 entries with diverse prefixes and depths + entries := make([]interface{}, 200) + prefixes := []string{ + "/etc", "/usr", "/usr/bin", "/usr/lib", "/usr/share", "/usr/local", + "/var", "/var/log", "/var/run", "/var/lib", "/var/cache", + "/lib", "/lib64", "/opt", "/home", "/tmp", "/run", "/sys", "/proc", "/dev", + "/srv", "/mnt", "/boot", "/root", "/sbin", "/bin", + } + for i := 0; i < 200; i++ { + base := prefixes[i%len(prefixes)] + depth := i / len(prefixes) // 0-7 depending on iteration + p := base + for d := 0; d < depth; d++ { + p = fmt.Sprintf("%s/sub%d", p, d) + } + threshold := (i%20)*5 + 1 // varied thresholds: 1, 6, 11, ..., 96 + entries[i] = map[string]interface{}{ + "prefix": p, + "threshold": int64(threshold), + } + } + + cc := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "CollapseConfiguration", + "metadata": map[string]interface{}{"name": ccName}, + "spec": map[string]interface{}{ + "openDynamicThreshold": int64(50), + "endpointDynamicThreshold": int64(100), + "collapseConfigs": entries, + }, + }, + } + + _, err := dynClient.Resource(collapseGVR).Create(ctx, cc, metav1.CreateOptions{}) + require.NoError(t, err, "create CollapseConfiguration with 200 entries") + + // Verify storage accepted it + got, err := dynClient.Resource(collapseGVR).Get(ctx, ccName, metav1.GetOptions{}) + require.NoError(t, err, "get CollapseConfiguration") + storedConfigs := got.Object["spec"].(map[string]interface{})["collapseConfigs"].([]interface{}) + require.Len(t, storedConfigs, 200, "all 200 entries must be stored") + t.Logf("Stored 200 entries, first: %v, last: %v", storedConfigs[0], storedConfigs[199]) + + // Deploy nginx and learn AP with the 200-entry config + ns := testutils.NewRandomNamespace() + t.Logf("Using namespace %s", ns.Name) + wl, err := testutils.NewTestWorkload(ns.Name, path.Join(utils.CurrentDir(), "resources/nginx-deployment.yaml")) + require.NoError(t, err, "create nginx workload") + require.NoError(t, wl.WaitForReady(30)) + + time.Sleep(10 * time.Second) + + err = wl.WaitForApplicationProfileCompletion(30) + require.NoError(t, err, "wait for AP completion with 200-entry config") + + ap, err := wl.GetApplicationProfile() + require.NoError(t, err, "get AP") + require.NotEmpty(t, ap.Spec.Containers) + + for _, container := range ap.Spec.Containers { + t.Logf("200-entry test — Container %q: %d opens", container.Name, len(container.Opens)) + for _, o := range container.Opens { + t.Logf(" open: %s (flags: %v)", o.Path, o.Flags) + } + // With 200 entries, the /etc entry has threshold=1 (i=0, (0%20)*5+1=1) + // meaning every unique child under /etc should collapse immediately + var collapsedEtc bool + for _, o := range container.Opens { + if strings.HasPrefix(o.Path, "/etc/") && + (strings.Contains(o.Path, "⋯") || strings.Contains(o.Path, "*")) { + collapsedEtc = true + } + } + // /etc has threshold=1, so any child should be collapsed + assert.True(t, collapsedEtc, "/etc should collapse with threshold=1") + } + t.Log("200-entry stress test verified — storage handles large CollapseConfig correctly") + }) +} diff --git a/tests/resources/crypto-miner-deployment.yaml b/tests/resources/crypto-miner-deployment.yaml new file mode 100644 index 0000000000..382a3cb995 --- /dev/null +++ b/tests/resources/crypto-miner-deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8s-miner-deployment + labels: + app: k8s-miner +spec: + replicas: 1 + selector: + matchLabels: + app: k8s-miner + template: + metadata: + labels: + app: k8s-miner + kubescape.io/user-defined-profile: crypto2 + spec: + containers: + - name: k8s-miner + image: docker.io/amitschendel/crypto-miner-1 + imagePullPolicy: Always + workingDir: /usr/app/src + command: ["./xmrig"] + args: ["--bench", "1M"] diff --git a/tests/resources/curl-plain-deployment.yaml b/tests/resources/curl-plain-deployment.yaml new file mode 100644 index 0000000000..003810550a --- /dev/null +++ b/tests/resources/curl-plain-deployment.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: curl-fusioncore-28-0 + name: curl-fusioncore-deployment +spec: + selector: + matchLabels: + app: curl-fusioncore-28-0 + replicas: 1 + template: + metadata: + labels: + app: curl-fusioncore-28-0 + spec: + containers: + - name: curl + image: docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058 + command: ["sleep", "infinity"] diff --git a/tests/resources/curl-signed-deployment.yaml b/tests/resources/curl-signed-deployment.yaml new file mode 100644 index 0000000000..df15283ccd --- /dev/null +++ b/tests/resources/curl-signed-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: curl-29 + name: curl-29 +spec: + selector: + matchLabels: + app: curl-29 + replicas: 1 + template: + metadata: + labels: + app: curl-29 + kubescape.io/user-defined-profile: signed-ap + spec: + containers: + - name: curl + image: docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058 + command: ["sleep", "infinity"] diff --git a/tests/resources/curl-user-network-deployment.yaml b/tests/resources/curl-user-network-deployment.yaml new file mode 100644 index 0000000000..122de0f1c1 --- /dev/null +++ b/tests/resources/curl-user-network-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: curl-fusioncore-28-1 + name: curl-fusioncore-deployment +spec: + selector: + matchLabels: + app: curl-fusioncore-28-1 + replicas: 1 + template: + metadata: + labels: + app: curl-fusioncore-28-1 + kubescape.io/user-defined-network: fusioncore-network + spec: + containers: + - name: curl + image: docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058 + command: ["sleep", "infinity"] diff --git a/tests/resources/curl-user-profile-wildcards-deployment.yaml b/tests/resources/curl-user-profile-wildcards-deployment.yaml new file mode 100644 index 0000000000..7b2e4ab7db --- /dev/null +++ b/tests/resources/curl-user-profile-wildcards-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: curl-fusioncore + name: curl-fusioncore-deployment +spec: + selector: + matchLabels: + app: curl-fusioncore + replicas: 1 + template: + metadata: + labels: + app: curl-fusioncore + kubescape.io/user-defined-profile: fusioncore-profile-wildcards + spec: + containers: + - name: curl + image: docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058 + command: ["sleep", "infinity"] diff --git a/tests/resources/known-network-neighborhood.yaml b/tests/resources/known-network-neighborhood.yaml new file mode 100644 index 0000000000..0d4caa0c4e --- /dev/null +++ b/tests/resources/known-network-neighborhood.yaml @@ -0,0 +1,49 @@ +## +## User-defined NetworkNeighborhood for Test_28. +## +## Referenced directly from a pod via the label: +## kubescape.io/user-defined-network: fusioncore-network +## +## Carries "kubescape.io/managed-by: User" annotation and workload +## labels to match the schema the node-agent cache expects. +## +## Modeled after a real auto-learned NN from curlimages/curl:8.5.0. +## +## Usage: +## sed "s/{{NAMESPACE}}/$NS/g" known-network-neighborhood.yaml \ +## | kubectl apply -f - +## +apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1 +kind: NetworkNeighborhood +metadata: + name: fusioncore-network + namespace: "{{NAMESPACE}}" + annotations: + kubescape.io/managed-by: User + kubescape.io/status: completed + kubescape.io/completion: complete + labels: + kubescape.io/workload-api-group: apps + kubescape.io/workload-api-version: v1 + kubescape.io/workload-kind: Deployment + kubescape.io/workload-name: curl-fusioncore-deployment + kubescape.io/workload-namespace: "{{NAMESPACE}}" +spec: + matchLabels: + app: curl-fusioncore-28-1 + containers: + - name: curl + ingress: [] + egress: + - dns: fusioncore.ai. + dnsNames: + - fusioncore.ai. + identifier: a5e64ff1db824089b1706ac872303e55075f92cf6a652b5272f06c3a2b9e8d10 + ipAddress: 162.0.217.171 + namespaceSelector: null + podSelector: null + ports: + - name: TCP-80 + port: 80 + protocol: TCP + type: external diff --git a/tests/resources/nginx-user-defined-deployment.yaml b/tests/resources/nginx-user-defined-deployment.yaml new file mode 100644 index 0000000000..8e68df16bb --- /dev/null +++ b/tests/resources/nginx-user-defined-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: curl-28 + name: curl-28 +spec: + selector: + matchLabels: + app: curl-28 + replicas: 1 + template: + metadata: + labels: + app: curl-28 + kubescape.io/user-defined-network: curl-nn + kubescape.io/user-defined-profile: curl-ap + spec: + containers: + - name: curl + image: docker.io/curlimages/curl@sha256:08e466006f0860e54fc299378de998935333e0e130a15f6f98482e9f8dab3058 + command: ["sleep", "infinity"] diff --git a/tests/resources/nginx-user-profile-deployment.yaml b/tests/resources/nginx-user-profile-deployment.yaml new file mode 100644 index 0000000000..218f956540 --- /dev/null +++ b/tests/resources/nginx-user-profile-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 1 + template: + metadata: + labels: + app: nginx + kubescape.io/user-defined-profile: nginx-regex-profile + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/tests/resources/run-test-28.sh b/tests/resources/run-test-28.sh new file mode 100755 index 0000000000..582aac83cd --- /dev/null +++ b/tests/resources/run-test-28.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# +# run-test-28.sh — Manual execution of Test_28_UserDefinedNetworkNeighborhood +# +# Applies user-defined NN, deploys curl, waits for AP to auto-learn, +# triggers allowed + unknown traffic, checks for alerts. +# +# Usage: +# ./run-test-28.sh # run the test +# ./run-test-28.sh learn # learn NN from scratch (debug) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ALERTMANAGER_URL="${ALERTMANAGER_URL:-http://localhost:9093}" +SCENARIO="${1:-test}" + +# Ensure alertmanager port-forward is active. +if ! curl -s --max-time 2 "$ALERTMANAGER_URL/api/v2/alerts" >/dev/null 2>&1; then + echo "Alertmanager not reachable at $ALERTMANAGER_URL — starting port-forward..." + kubectl port-forward svc/alertmanager-operated 9093:9093 -n monitoring & + ALERT_PF_PID=$! + sleep 3 + if ! curl -s --max-time 2 "$ALERTMANAGER_URL/api/v2/alerts" >/dev/null 2>&1; then + echo "ERROR: alertmanager still not reachable after port-forward (pid=$ALERT_PF_PID)" + kill "$ALERT_PF_PID" 2>/dev/null || true + exit 1 + fi + trap 'kill $ALERT_PF_PID 2>/dev/null || true' EXIT +fi + +get_all_alerts() { + local ns="$1" + curl -s "$ALERTMANAGER_URL/api/v2/alerts" | \ + jq "[.[] | select(.labels.namespace==\"$ns\")]" +} + +wait_for_pod() { + local ns="$1" + kubectl rollout status deployment/curl-fusioncore-deployment -n "$ns" --timeout=120s +} + +get_pod() { + local ns="$1" + kubectl get pods -n "$ns" -l app=curl-fusioncore-28-1 \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null +} + +# ================================================================= +# Main test: apply NN manifest, deploy curl, trigger traffic, check alerts +# ================================================================= +run_test() { + local NS="t28-$(head -c4 /dev/urandom | xxd -p)" + local NET="fusioncore-network-$NS" + echo "" + echo "=== Test 28: User-Defined NN (ns=$NS, net=$NET) ===" + + kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - + + # 1. Apply NN manifest with unique name + sed -e "s/{{NAMESPACE}}/$NS/g" \ + -e "s/fusioncore-network/$NET/g" \ + "$SCRIPT_DIR/known-network-neighborhood.yaml" | kubectl apply -f - + echo " NN $NET created" + + # 2. Deploy curl with user-defined-network label + sed "s/{{NETWORK_NAME}}/$NET/g" \ + "$SCRIPT_DIR/curl-user-network-deployment.yaml" | kubectl apply -n "$NS" -f - + wait_for_pod "$NS" + local POD; POD=$(get_pod "$NS") + echo " Pod: $POD" + + # 3. Wait for AP to auto-learn + echo " Waiting for AP to complete..." + for i in $(seq 1 80); do + AP_STATUS=$(kubectl get applicationprofiles -n "$NS" \ + -o jsonpath='{.items[0].metadata.annotations.kubescape\.io/status}' 2>/dev/null || true) + [ "$AP_STATUS" = "completed" ] && break + sleep 10 + done + echo " AP status: $AP_STATUS" + + # 4. Trigger traffic + echo " Triggering traffic..." + echo " nslookup fusioncore.ai (allowed)" + kubectl exec -n "$NS" "$POD" -c curl -- nslookup fusioncore.ai 2>&1 || true + echo " curl fusioncore.ai (allowed)" + kubectl exec -n "$NS" "$POD" -c curl -- curl -sm2 http://fusioncore.ai >/dev/null 2>&1 || true + echo " nslookup evil.example.com (unknown)" + kubectl exec -n "$NS" "$POD" -c curl -- nslookup evil.example.com 2>&1 || true + echo " curl evil.example.com (unknown)" + kubectl exec -n "$NS" "$POD" -c curl -- curl -sm2 http://evil.example.com >/dev/null 2>&1 || true + + echo " Waiting 30s for alerts..." + sleep 30 + + # 5. Check alerts + echo "" + echo " === All alerts in namespace $NS ===" + ALERTS=$(get_all_alerts "$NS") + ALERT_COUNT=$(echo "$ALERTS" | jq 'length') + echo "$ALERTS" | jq -r '.[] | " [\(.labels.rule_name)] container=\(.labels.container_name // "n/a")"' + echo " Total: $ALERT_COUNT" + echo " ======================================" + + if [ "$ALERT_COUNT" -eq 0 ]; then + echo " FAIL: expected at least one alert (R0005 for evil.example.com), got ZERO" + echo " Namespace $NS left for inspection" + exit 1 + else + echo " PASS: got $ALERT_COUNT alert(s)" + echo " Cleanup: kubectl delete namespace $NS" + fi +} + +# ================================================================= +# Learn scenario: no user-defined labels, learn NN from scratch +# ================================================================= +run_learn() { + local NS="t28-learn-$(head -c4 /dev/urandom | xxd -p)" + echo "" + echo "=== LEARN NN from scratch (ns=$NS) ===" + + kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - + kubectl apply -n "$NS" -f "$SCRIPT_DIR/curl-plain-deployment.yaml" + wait_for_pod "$NS" + local POD; POD=$(kubectl get pods -n "$NS" -l app=curl-fusioncore-28-0 \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + echo " Pod: $POD" + + echo " Triggering traffic during learning window..." + kubectl exec -n "$NS" "$POD" -c curl -- nslookup fusioncore.ai 2>&1 || true + kubectl exec -n "$NS" "$POD" -c curl -- curl -sm5 http://fusioncore.ai >/dev/null 2>&1 || true + sleep 5 + kubectl exec -n "$NS" "$POD" -c curl -- nslookup fusioncore.ai 2>&1 || true + kubectl exec -n "$NS" "$POD" -c curl -- curl -sm5 http://fusioncore.ai >/dev/null 2>&1 || true + + echo " Waiting for NN to complete..." + for i in $(seq 1 80); do + NN_STATUS=$(kubectl get networkneighborhoods -n "$NS" \ + -o jsonpath='{.items[0].metadata.annotations.kubescape\.io/status}' 2>/dev/null || true) + [ "$NN_STATUS" = "completed" ] && break + sleep 10 + done + echo " NN status: $NN_STATUS" + + echo "" + echo " === Learned NetworkNeighborhood ===" + kubectl get networkneighborhoods -n "$NS" -o yaml 2>&1 + echo " ===================================" + echo "" + echo " Namespace $NS left for inspection" + echo " Cleanup: kubectl delete namespace $NS" +} + +case "$SCENARIO" in + test) run_test ;; + learn) run_learn ;; + *) + echo "Usage: $0 [test|learn]" + exit 1 + ;; +esac diff --git a/tests/resources/test-28-iterate.sh b/tests/resources/test-28-iterate.sh new file mode 100755 index 0000000000..355fdf65ff --- /dev/null +++ b/tests/resources/test-28-iterate.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# +# test-28-iterate.sh — Self-contained test for user-defined NN alerts +# +# Prerequisites: +# - R0011 must be enabled with isTriggerAlert=true in the Rules CRD +# - R0005 should have isTriggerAlert=true for DNS alerts +# - Alertmanager port-forward active on localhost:9093 +# +# Usage: ./test-28-iterate.sh +# +set -euo pipefail + +ALERTMANAGER_URL="${ALERTMANAGER_URL:-http://localhost:9093}" +NA_POD=$(kubectl get pods -n kubescape -l app=node-agent -o jsonpath='{.items[0].metadata.name}') + +# Ensure alertmanager reachable +if ! curl -s --max-time 2 "$ALERTMANAGER_URL/api/v2/alerts" >/dev/null 2>&1; then + echo "ERROR: alertmanager not reachable at $ALERTMANAGER_URL" + exit 1 +fi + +get_alerts() { + local ns="$1" + curl -s "$ALERTMANAGER_URL/api/v2/alerts" | jq "[.[] | select(.labels.namespace==\"$ns\")]" +} + +cleanup_ns() { + kubectl delete namespace "$1" --wait=false 2>/dev/null || true +} + +# ================================================================ +# Ensure R0005 and R0011 are enabled with isTriggerAlert=true +# ================================================================ +echo "Patching rules: R0005 isTriggerAlert=true, R0011 enabled+isTriggerAlert=true" +kubectl get rules -n kubescape default-rules -o json | jq ' + .spec.rules = [ + .spec.rules[] | + if .id == "R0005" then .isTriggerAlert = true + elif .id == "R0011" then .enabled = true | .isTriggerAlert = true + else . + end + ] +' | kubectl apply -f - >/dev/null 2>&1 +echo " Done" + +# ================================================================ +# TEST: User-defined AP + NN → R0011 for anomalous TCP egress +# ================================================================ +NS="t28-$(head -c4 /dev/urandom | xxd -p)" +echo "" +echo "============================================================" +echo "TEST: User-defined AP + NN → R0011 Unexpected Egress Traffic" +echo " ns=$NS" +echo "============================================================" + +kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - + +# Create user-defined ApplicationProfile +cat <&1 \ + | grep -c "added user-defined network neighborhood" || true) + GOT_AP=$(kubectl logs "$NA_POD" -n kubescape -c node-agent --since=30s 2>&1 \ + | grep -c "added user-defined application profile\|user defined profile" || true) + echo " NN=$GOT_NN AP=$GOT_AP" + [ "$GOT_NN" -gt 0 ] && [ "$GOT_AP" -gt 0 ] && break + sleep 3 +done + +# Trigger anomalous TCP egress (R0011) +echo "" +echo " [anomaly] curl -sm5 http://8.8.8.8 (NOT in NN egress)" +kubectl exec -n "$NS" "$POD" -c nginx -- curl -sm5 http://8.8.8.8 2>&1 || true +echo " [anomaly] curl -sm5 http://1.1.1.1 (NOT in NN egress)" +kubectl exec -n "$NS" "$POD" -c nginx -- curl -sm5 http://1.1.1.1 2>&1 || true + +# Poll for alerts +echo "" +echo " Polling for alerts..." +R0011_ALERTS=0 +for i in 1 2 3 4; do + sleep 5 + ALERTS=$(get_alerts "$NS") + ALERT_COUNT=$(echo "$ALERTS" | jq 'length') + R0011_ALERTS=$(echo "$ALERTS" | jq '[.[] | select(.labels.rule_id=="R0011")] | length') + echo " poll $i: total=$ALERT_COUNT R0011=$R0011_ALERTS" + [ "$R0011_ALERTS" -gt 0 ] && break +done + +echo "" +echo " === All alerts in $NS ===" +echo "$ALERTS" | jq -r '.[] | " [\(.labels.rule_id)] \(.labels.rule_name) | comm=\(.labels.comm // "?")"' 2>/dev/null || true +echo " Total: $ALERT_COUNT R0011: $R0011_ALERTS" +echo " ========================" + +if [ "$R0011_ALERTS" -gt 0 ]; then + echo "" + echo " >>> RESULT: PASS — R0011 fires for user-defined AP+NN" + cleanup_ns "$NS" + exit 0 +else + echo "" + echo " >>> RESULT: FAIL — no R0011 alerts" + echo " >>> Namespace $NS left for inspection" + exit 1 +fi diff --git a/tests/resources/user-profile.yaml b/tests/resources/user-profile.yaml deleted file mode 100644 index 97a116f6d2..0000000000 --- a/tests/resources/user-profile.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1 -kind: ApplicationProfile -metadata: - name: {name} - namespace: {namespace} - resourceVersion: "1" # Start with "1" for new resources - annotations: - kubescape.io/managed-by: User -spec: - architectures: ["amd64"] - containers: - - name: nginx - imageID: "" - imageTag: "" - capabilities: [] - opens: [] - syscalls: [] - endpoints: [] - execs: - - path: /usr/bin/ls - args: - - /usr/bin/ls - - -l - seccompProfile: - spec: - defaultAction: "" - - name: server - imageID: "" - imageTag: "" - capabilities: [] - opens: [] - syscalls: [] - endpoints: [] - execs: - - path: /bin/ls - args: - - /bin/ls - - -l - - path: /bin/grpc_health_probe - args: - - "-addr=:9555" - seccompProfile: - spec: - defaultAction: "" - initContainers: [] - ephemeralContainers: [] -status: {} \ No newline at end of file diff --git a/tests/scripts/local-ci.sh b/tests/scripts/local-ci.sh new file mode 100755 index 0000000000..c226f5f611 --- /dev/null +++ b/tests/scripts/local-ci.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# local-ci.sh — exact local mirror of .github/workflows/component-tests.yaml +# +# Usage: +# ./tests/scripts/local-ci.sh # full run: cluster setup + deploy + all tests +# ./tests/scripts/local-ci.sh --deploy-only # stop after helm install (skip tests) +# ./tests/scripts/local-ci.sh --test-only Test_27 # skip setup, just run one test +# ./tests/scripts/local-ci.sh Test_27 # full run, single test +# ./tests/scripts/local-ci.sh Test_01 Test_27 # full run, multiple tests +# +# Differences from CI: +# - Uses ~/go/bin/kind (v0.31.0) instead of downloading kind +# - Uses existing kubectl instead of downloading it +# - Builds Docker images locally + kind load instead of pulling from ghcr.io +# - Storage is built from ../storage (local replace for go.mod) +# - Node-agent privileged=true for Kind clusters +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +STORAGE_ROOT="$(cd "$REPO_ROOT/../storage" && pwd)" +KIND="${HOME}/go/bin/kind" +KIND_CLUSTER="integration-test" +NAMESPACE="kubescape" +TAG="local-test" + +cd "$REPO_ROOT" + +# ── parse args ──────────────────────────────────────────────────────────────── +DEPLOY_ONLY=false +TEST_ONLY=false +TESTS=() + +for arg in "$@"; do + case "$arg" in + --deploy-only) DEPLOY_ONLY=true ;; + --test-only) TEST_ONLY=true ;; + Test_*) TESTS+=("$arg") ;; + esac +done + +log() { echo "[$(date +%H:%M:%S)] $*"; } + +# ── step 1: kind cluster ───────────────────────────────────────────────────── +# CI: curl kind, ./kind create cluster, download kubectl +setup_cluster() { + if $KIND get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER}$"; then + log "Kind cluster '${KIND_CLUSTER}' already exists, reusing" + else + log "Creating Kind cluster '${KIND_CLUSTER}'" + $KIND create cluster --name "$KIND_CLUSTER" + fi + kubectl cluster-info --context "kind-${KIND_CLUSTER}" >/dev/null 2>&1 \ + || { log "ERROR: cluster unreachable"; exit 1; } +} + +# ── step 2: install prometheus ──────────────────────────────────────────────── +# CI: helm repo add + helm upgrade --install prometheus +install_prometheus() { + if kubectl get ns monitoring >/dev/null 2>&1 && \ + kubectl get pods -n monitoring -l app.kubernetes.io/name=prometheus -o name 2>/dev/null | grep -q .; then + log "Prometheus already installed, skipping" + else + log "Installing Prometheus" + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 2>/dev/null || true + helm repo update + helm upgrade --install prometheus prometheus-community/kube-prometheus-stack \ + --set grafana.enabled=false \ + --namespace monitoring --create-namespace \ + --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false,prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \ + --set prometheus.prometheusSpec.maximumStartupDurationSeconds=300 \ + --wait --timeout 5m + fi + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=prometheus -n monitoring --timeout=300s +} + +# ── step 3: build + load images ────────────────────────────────────────────── +# CI: pulls from ghcr.io. Locally: docker build + kind load. +build_and_load_images() { + log "Checking disk space" + df -h / | tail -1 + + # Storage + log "Building storage image from ${STORAGE_ROOT}" + docker build -f "${STORAGE_ROOT}/build/Dockerfile" \ + -t "ghcr.io/k8sstormcenter/storage:${TAG}" \ + "${STORAGE_ROOT}" + + # Node-agent: add local storage replace, vendor, build + log "Setting up node-agent go.mod with local storage replace" + go mod edit -replace "github.com/kubescape/storage=${STORAGE_ROOT}" + go mod tidy + go mod vendor + + log "Building node-agent image" + docker build -f build/Dockerfile \ + -t "ghcr.io/k8sstormcenter/node-agent:${TAG}" \ + --build-arg image_version="${TAG}" . + + # Clean up Docker build cache + docker builder prune --filter until=1h -f >/dev/null 2>&1 || true + docker image prune -f >/dev/null 2>&1 || true + + # Drop the local replace (not committed) + go mod edit -dropreplace "github.com/kubescape/storage" + + log "Loading images into Kind" + $KIND load docker-image "ghcr.io/k8sstormcenter/storage:${TAG}" --name "$KIND_CLUSTER" + $KIND load docker-image "ghcr.io/k8sstormcenter/node-agent:${TAG}" --name "$KIND_CLUSTER" + + log "Checking disk space after build" + df -h / | tail -1 +} + +# ── step 4: helm install kubescape ──────────────────────────────────────────── +# CI: helm upgrade --install kubescape ./tests/chart --set ... +install_kubescape() { + log "Installing kubescape chart (storage=${TAG}, node-agent=${TAG})" + helm upgrade --install kubescape ./tests/chart \ + --set clusterName="$(kubectl config current-context)" \ + --set nodeAgent.image.tag="${TAG}" \ + --set nodeAgent.image.repository=ghcr.io/k8sstormcenter/node-agent \ + --set nodeAgent.image.pullPolicy=Never \ + --set storage.image.tag="${TAG}" \ + --set storage.image.pullPolicy=Never \ + --set nodeAgent.privileged=true \ + -n "$NAMESPACE" --create-namespace --wait --timeout 5m \ + --disable-openapi-validation + + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=node-agent -n "$NAMESPACE" --timeout=300s + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=storage -n "$NAMESPACE" --timeout=300s + sleep 5 +} + +# ── step 5: port forwarding ────────────────────────────────────────────────── +# CI: ./tests/scripts/port-forward.sh +start_port_forwards() { + log "Setting up port forwarding" + # Kill stale port-forwards + pkill -f 'kubectl port-forward.*alertmanager-operated' 2>/dev/null || true + pkill -f 'kubectl port-forward.*prometheus-kube-prometheus' 2>/dev/null || true + sleep 1 + "${SCRIPT_DIR}/port-forward.sh" +} + +# ── step 6: run tests ──────────────────────────────────────────────────────── +# CI: cd tests && go test -v ./... -run ${{ matrix.test }} --timeout=20m --tags=component +run_tests() { + local test_pattern="${1:-}" + + # CI: Update storage dependency (go mod edit -replace ...) + # Locally: same replace so test code compiles against fork storage + log "Applying storage replace for test compilation" + go mod edit -replace "github.com/kubescape/storage=${STORAGE_ROOT}" + go mod tidy + go mod vendor + + if [[ -n "$test_pattern" ]]; then + log "Running test: ${test_pattern}" + cd tests && CGO_ENABLED=0 go test -v ./... -run "${test_pattern}" --timeout=20m --tags=component; cd .. + else + log "Running all component tests" + cd tests && CGO_ENABLED=0 go test -v ./... --timeout=20m --tags=component; cd .. + fi +} + +# ── step 7: collect logs ───────────────────────────────────────────────────── +# CI: kubectl logs ... node-agent + storage +print_logs() { + log "=== Node agent logs ===" + kubectl logs "$(kubectl get pods -n "$NAMESPACE" -o name | grep node-agent)" -n "$NAMESPACE" -c node-agent --tail=100 2>/dev/null || true + echo "-----------------------------------------" + log "=== Storage logs ===" + kubectl logs "$(kubectl get pods -n "$NAMESPACE" -o name | grep storage)" -n "$NAMESPACE" --tail=50 2>/dev/null || true +} + +# ── main ────────────────────────────────────────────────────────────────────── +if $TEST_ONLY; then + # Just run the test(s), assume cluster + deploy are already done + start_port_forwards + if [[ ${#TESTS[@]} -gt 0 ]]; then + for t in "${TESTS[@]}"; do run_tests "$t"; done + else + run_tests "" + fi + print_logs + exit 0 +fi + +setup_cluster +install_prometheus +build_and_load_images +install_kubescape + +if $DEPLOY_ONLY; then + log "Deploy complete. Pods:" + kubectl get pods -n "$NAMESPACE" -o wide + exit 0 +fi + +start_port_forwards + +# Run specified tests or all +set +e +if [[ ${#TESTS[@]} -gt 0 ]]; then + for t in "${TESTS[@]}"; do run_tests "$t"; done +else + run_tests "" +fi +TEST_EXIT=$? +set -e + +print_logs + +if [[ "$TEST_EXIT" -eq 0 ]]; then + log "All tests passed" +else + log "Tests finished with exit code ${TEST_EXIT}" +fi +exit $TEST_EXIT diff --git a/tests/scripts/storage-tag.sh b/tests/scripts/storage-tag.sh index 5512f2ae5a..4bb2565eb8 100755 --- a/tests/scripts/storage-tag.sh +++ b/tests/scripts/storage-tag.sh @@ -1,4 +1,2 @@ -#/bin/bash -curl -s https://raw.githubusercontent.com/kubescape/helm-charts/main/charts/kubescape-operator/values.yaml -o values.yaml -yq '.storage.image.tag' < values.yaml -rm -rf values.yaml +#!/bin/bash +curl -s https://api.github.com/repos/k8sstormcenter/storage/tags | jq -r '.[0].name' diff --git a/tests/testutils/log_alerts.go b/tests/testutils/log_alerts.go new file mode 100644 index 0000000000..70d11ec31e --- /dev/null +++ b/tests/testutils/log_alerts.go @@ -0,0 +1,204 @@ +package testutils + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/kubescape/k8s-interface/k8sinterface" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" +) + +// LogAlert represents a full alert parsed from node-agent stdout exporter logs. +type LogAlert struct { + RuleID string + AlertName string + Namespace string + Container string + PodName string + Comm string + // DNS-specific (R0005) + Domain string + Addresses []string + // Network-specific (R0011) + DstIP string + Port string + Proto string + // Full message from arguments + Message string +} + +// GetLogAlerts reads node-agent pod logs, parses the stdout exporter JSON, +// and returns alerts filtered by namespace. +func GetLogAlerts(namespace string) ([]LogAlert, error) { + k8sClient := k8sinterface.NewKubernetesApi() + + pods, err := k8sClient.KubernetesClient.CoreV1().Pods("").List( + context.TODO(), metav1.ListOptions{ + LabelSelector: "app=node-agent", + }) + if err != nil { + return nil, fmt.Errorf("list node-agent pods: %w", err) + } + + var alerts []LogAlert + for _, pod := range pods.Items { + buf := &bytes.Buffer{} + req := k8sClient.KubernetesClient.CoreV1().RESTClient(). + Get(). + Namespace(pod.Namespace). + Name(pod.Name). + Resource("pods"). + SubResource("log"). + VersionedParams(&v1.PodLogOptions{ + Container: "node-agent", + }, scheme.ParameterCodec) + + rc, err := req.Stream(context.TODO()) + if err != nil { + return nil, fmt.Errorf("stream logs for %s: %w", pod.Name, err) + } + _, err = io.Copy(buf, rc) + rc.Close() + if err != nil { + return nil, fmt.Errorf("read logs for %s: %w", pod.Name, err) + } + + parsed := parseLogAlerts(buf.String(), namespace) + alerts = append(alerts, parsed...) + } + return alerts, nil +} + +// parseLogAlerts extracts alert entries from logrus JSON lines. +func parseLogAlerts(logs, namespace string) []LogAlert { + var alerts []LogAlert + for _, line := range strings.Split(logs, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + var entry map[string]interface{} + if err := json.Unmarshal([]byte(line), &entry); err != nil { + continue + } + + ruleID, _ := entry["RuleID"].(string) + if ruleID == "" { + continue + } + + // Extract RuntimeK8sDetails + k8s, _ := entry["RuntimeK8sDetails"].(map[string]interface{}) + ns, _ := k8s["namespace"].(string) + if namespace != "" && ns != namespace { + continue + } + + container, _ := k8s["containerName"].(string) + podName, _ := k8s["podName"].(string) + + // Extract BaseRuntimeMetadata + base, _ := entry["BaseRuntimeMetadata"].(map[string]interface{}) + alertName, _ := base["alertName"].(string) + args, _ := base["arguments"].(map[string]interface{}) + + // Extract identifiers for comm + comm := "" + if ids, ok := base["identifiers"].(map[string]interface{}); ok { + if proc, ok := ids["process"].(map[string]interface{}); ok { + comm, _ = proc["name"].(string) + } + } + + la := LogAlert{ + RuleID: ruleID, + AlertName: alertName, + Namespace: ns, + Container: container, + PodName: podName, + Comm: comm, + } + + if args != nil { + la.Domain, _ = args["domain"].(string) + la.Message, _ = args["message"].(string) + la.Proto, _ = args["protocol"].(string) + if la.Proto == "" { + la.Proto, _ = args["proto"].(string) + } + + // IP: from arguments.ip (R0011) or identifiers.network.dstIP (R0005) + if ip, ok := args["ip"].(string); ok { + la.DstIP = ip + } else if ids, ok := base["identifiers"].(map[string]interface{}); ok { + if net, ok := ids["network"].(map[string]interface{}); ok { + la.DstIP, _ = net["dstIP"].(string) + } + } + + // Port + switch p := args["port"].(type) { + case float64: + la.Port = fmt.Sprintf("%d", int(p)) + case string: + la.Port = p + } + + // Addresses (DNS) + if addrs, ok := args["addresses"].([]interface{}); ok { + for _, a := range addrs { + if s, ok := a.(string); ok { + la.Addresses = append(la.Addresses, s) + } + } + } + } + + alerts = append(alerts, la) + } + return alerts +} + +// FilterLogAlerts returns alerts matching the given rule ID. +func FilterLogAlerts(alerts []LogAlert, ruleID string) []LogAlert { + var out []LogAlert + for _, a := range alerts { + if a.RuleID == ruleID { + out = append(out, a) + } + } + return out +} + +// LogAlertDomains returns unique domain values from a set of log alerts. +func LogAlertDomains(alerts []LogAlert) []string { + seen := map[string]bool{} + var out []string + for _, a := range alerts { + if a.Domain != "" && !seen[a.Domain] { + seen[a.Domain] = true + out = append(out, a.Domain) + } + } + return out +} + +// LogAlertIPs returns unique DstIP values from a set of log alerts. +func LogAlertIPs(alerts []LogAlert) []string { + seen := map[string]bool{} + var out []string + for _, a := range alerts { + if a.DstIP != "" && !seen[a.DstIP] { + seen[a.DstIP] = true + out = append(out, a.DstIP) + } + } + return out +}