From 86bd3eaa1ef50d8f7e2f84ea30e2e3a88fc39d89 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Tue, 9 Sep 2025 08:50:53 +0200 Subject: [PATCH 01/10] Rewrite pull-secret-extractor in go Signed-off-by: Patrick Weiss --- .github/workflows/ci.yaml | 16 +- .../pull-secret-extractor/.dockerignore | 3 +- .../pull-secret-extractor/Dockerfile | 35 +- .../kubernetes/pull-secret-extractor/Makefile | 6 +- .../pull-secret-extractor/Taskfile.yaml | 85 +++++ .../pull-secret-extractor/docker_image.py | 33 -- .../kubernetes/pull-secret-extractor/go.mod | 48 +++ .../kubernetes/pull-secret-extractor/go.sum | 153 +++++++++ .../internal/docker_image/docker_image.go | 46 +++ .../docker_image/docker_image_test.go | 45 +++ .../secret_extraction/secret_extraction.go | 287 +++++++++++++++++ .../secret_extraction_test.go | 298 ++++++++++++++++++ .../kubernetes/pull-secret-extractor/main.go | 84 +++++ .../pull-secret-extractor/requirements.txt | 17 - .../requirements.txt.license | 3 - .../secret_extraction.py | 128 -------- .../integration}/test-pod.sh | 5 +- .../secrets}/secret_1/.dockerconfigjson | 0 .../secret_1/.dockerconfigjson.license | 0 .../secret_1/not_a_docker_config_json | 0 .../secret_1/not_a_docker_config_json.license | 0 .../secrets}/secret_2/.dockerconfigjson | 0 .../secret_2/.dockerconfigjson.license | 0 .../test_docker_image.py | 29 -- .../test_secret_extraction.py | 65 ---- 25 files changed, 1089 insertions(+), 297 deletions(-) create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/docker_image.py create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/go.mod create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/go.sum create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image.go create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image_test.go create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/main.go delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/requirements.txt delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/requirements.txt.license delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/secret_extraction.py rename auto-discovery/kubernetes/pull-secret-extractor/{integration-test => test/integration}/test-pod.sh (93%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_1/.dockerconfigjson (100%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_1/.dockerconfigjson.license (100%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_1/not_a_docker_config_json (100%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_1/not_a_docker_config_json.license (100%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_2/.dockerconfigjson (100%) rename auto-discovery/kubernetes/pull-secret-extractor/{test_secrets => test/testdata/secrets}/secret_2/.dockerconfigjson.license (100%) delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/test_docker_image.py delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/test_secret_extraction.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 746c3e878d..b7d89d9f58 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -275,7 +275,13 @@ jobs: - name: Setup Python Version uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: "${{ env.PYTHON_VERSION }}" + go-version-file: "auto-discovery/kubernetes/go.mod" + + - name: Lint Go Code + working-directory: ./auto-discovery/kubernetes + run: | + go fmt ./... + go vet ./... - name: Download Kind uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 @@ -309,15 +315,15 @@ jobs: - name: Unit Tests working-directory: ./auto-discovery/kubernetes/pull-secret-extractor - run: make unit-test + run: task unit-test - name: Build Container Image working-directory: ./auto-discovery/kubernetes/pull-secret-extractor - run: make docker-build + run: task docker-build - name: Export Container Image working-directory: ./auto-discovery/kubernetes/pull-secret-extractor - run: make docker-export + run: task docker-export - name: Upload Image As Artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -339,7 +345,7 @@ jobs: - name: "Run integration tests" working-directory: ./auto-discovery/kubernetes/pull-secret-extractor run: | - make integration-test + task integration-test # ---- Build Stage | AutoDiscovery | Cloud | AWS ---- auto-discovery-cloud-aws: diff --git a/auto-discovery/kubernetes/pull-secret-extractor/.dockerignore b/auto-discovery/kubernetes/pull-secret-extractor/.dockerignore index ee0eeed1b3..c8ebc7c57a 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/.dockerignore +++ b/auto-discovery/kubernetes/pull-secret-extractor/.dockerignore @@ -2,5 +2,4 @@ // // SPDX-License-Identifier: Apache-2.0 -integration-test/* -venv/* \ No newline at end of file +venv/* diff --git a/auto-discovery/kubernetes/pull-secret-extractor/Dockerfile b/auto-discovery/kubernetes/pull-secret-extractor/Dockerfile index ddd11c3b14..45500ca3b3 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/Dockerfile +++ b/auto-discovery/kubernetes/pull-secret-extractor/Dockerfile @@ -2,12 +2,29 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM python:3.13-alpine - -RUN addgroup -g 1001 nikto \ - && adduser -G nikto -s /bin/sh -D -u 1001 nikto -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY --chown=root:root --chmod=755 docker_image.py secret_extraction.py ./ -USER 1001 -CMD ["python", "secret_extraction.py"] +# Build the pull-secret-extractor binary +FROM --platform=$BUILDPLATFORM golang:1.25.0 AS builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY main.go main.go +COPY internal/ internal/ + +# Build +ARG TARGETOS TARGETARCH +RUN GOOS="$TARGETOS" GOARCH="$TARGETARCH" CGO_ENABLED=0 go build -a -o secret_extraction main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/secret_extraction . + +ENTRYPOINT ["/secret_extraction"] diff --git a/auto-discovery/kubernetes/pull-secret-extractor/Makefile b/auto-discovery/kubernetes/pull-secret-extractor/Makefile index 2f2b2b0dbb..a5bb390cf0 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/Makefile +++ b/auto-discovery/kubernetes/pull-secret-extractor/Makefile @@ -38,14 +38,14 @@ test: unit-test integration-test .PHONY: unit-test unit-test: - $(PYTHON) -m unittest discover + go test ./... .PHONY: integration-test integration-test: docker-build docker-export kind-import @echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'." kubectl delete namespace integration-test --wait || true kubectl create namespace integration-test - + ./integration-test/test-pod.sh ${IMG_NS}/${IMG}:${IMG_TAG} kubectl wait --for=condition=ready --timeout=60s -n integration-test pod/init-container-test @@ -73,4 +73,4 @@ docker-export: .PHONY: kind-import kind-import: @echo ".: 💾 Importing the image archive to local kind cluster." - kind load image-archive ./$(IMG).tar \ No newline at end of file + kind load image-archive ./$(IMG).tar diff --git a/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml b/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml new file mode 100644 index 0000000000..97d2dce7d7 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: the secureCodeBox authors +# +# SPDX-License-Identifier: Apache-2.0 + +version: "3.44.0" + +vars: + IMG_NS: '{{default "securecodebox" .IMG_NS}}' + IMG: '{{default "auto-discovery-secret-extractor" .IMG}}' + IMG_TAG: + sh: echo "${IMG_TAG:-sha-$(git rev-parse --short HEAD)}" + FULL_IMAGE: "{{.IMG_NS}}/{{.IMG}}/{{.IMG_TAG}}" + +tasks: + unit-test: + desc: Run unit tests + cmds: + - go test ./... + + integration-test: + desc: Run integration tests in kind cluster + deps: + - kind-import + cmds: + - defer: task clean + - 'echo "🩺 Starting integration test in kind namespace integration-tests."' + - cmd: kubectl delete namespace integration-test --wait + ignore_error: true + - kubectl create namespace integration-test + - ./test/integration/test-pod.sh {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} + - kubectl wait --for=condition=ready --timeout=60s -n integration-test pod/init-container-test + - kubectl get secret --namespace integration-test test-secret + + test: + desc: Run all tests (unit and integration) + deps: + - unit-test + - integration-test + + docker-build: + desc: Build docker image with the manager + cmds: + - 'echo "⚙️ Build Container Images"' + - docker build -t {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} . + + docker-push: + desc: Push docker image to registry + deps: + - docker-build + cmds: + - docker push {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} + + docker-export: + desc: Export container image to tar archive + deps: + - docker-build + cmds: + - 'echo "💾 Export Container Images"' + - docker save {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} > {{.IMG}}.tar + + kind-import: + desc: Import container image to local kind cluster + deps: + - docker-export + preconditions: + - sh: test -f {{.IMG}}.tar + msg: "Image archive {{.IMG}}.tar not found. Run 'task docker-export' first." + cmds: + - 'echo "💾 Importing the image archive to local kind cluster."' + - kind load image-archive ./{{.IMG}}.tar + + clean: + desc: Clean up generated files + cmds: + - rm -f {{.IMG}}.tar + + vars: + desc: Display current variable values (useful for debugging) + cmds: + - | + echo "Current variable values:" + echo " IMG_NS: {{.IMG_NS}}" + echo " IMG: {{.IMG}}" + echo " IMG_TAG: {{.IMG_TAG}}" + echo " FULL_IMAGE: {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}}" diff --git a/auto-discovery/kubernetes/pull-secret-extractor/docker_image.py b/auto-discovery/kubernetes/pull-secret-extractor/docker_image.py deleted file mode 100644 index d8a8039ff0..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/docker_image.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: the secureCodeBox authors -# -# SPDX-License-Identifier: Apache-2.0 - -legacyDefaultDomain = "index.docker.io" -defaultDomain = "docker.io" -officialRepoName = "library" -defaultTag = "latest" - - -def get_domain_from_docker_image(name: str) -> str: - """ - Extracts domain and image from a given docker image. Has the same defaulting behavior when it comes to docker.io image as containerd - Code adapted from https://github.com/containerd/containerd/blob/20de989afcd2fd4edc20e9b85312e49a8bbe152b/reference/docker/normalize.go#L102-L119 - :param name: docker image - :return: tuple container domain and image - """ - try: - i = name.index('/') - except ValueError: - i = -1 - - name_slice = name[:i] - if i == -1 or ':' not in name_slice and '.' not in name_slice and name_slice != 'localhost' and name_slice.lower() == name_slice: - domain = defaultDomain - else: - domain = name[:i] - - if domain == legacyDefaultDomain: - domain = defaultDomain - - return domain - diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.mod b/auto-discovery/kubernetes/pull-secret-extractor/go.mod new file mode 100644 index 0000000000..09b2ff99bb --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.mod @@ -0,0 +1,48 @@ +module github.com/secureCodeBox/auto-discovery/kubernetes/pull-secret-extractor + +go 1.24.5 + +require ( + k8s.io/api v0.33.4 + k8s.io/apimachinery v0.33.4 + k8s.io/client-go v0.33.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.sum b/auto-discovery/kubernetes/pull-secret-extractor/go.sum new file mode 100644 index 0000000000..1ca7012133 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.sum @@ -0,0 +1,153 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= +k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= +k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= +k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= +k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image.go new file mode 100644 index 0000000000..7993644919 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + +package docker_image + +import ( + "strings" +) + +const ( + legacyDefaultDomain = "index.docker.io" + defaultDomain = "docker.io" + officialRepoName = "library" + defaultTag = "latest" +) + +// GetDomainFromDockerImage extracts domain from a given docker image. +// Has the same defaulting behavior when it comes to docker.io image as containerd. +// Code adapted from https://github.com/containerd/containerd/blob/20de989afcd2fd4edc20e9b85312e49a8bbe152b/reference/docker/normalize.go#L102-L119 +func GetDomainFromDockerImage(name string) string { + i := strings.Index(name, "/") + + var domain string + + if i == -1 { + domain = defaultDomain + } else { + nameSlice := name[:i] + + if !strings.Contains(nameSlice, ":") && + !strings.Contains(nameSlice, ".") && + nameSlice != "localhost" && + strings.ToLower(nameSlice) == nameSlice { + domain = defaultDomain + } else { + domain = nameSlice + } + } + + if domain == legacyDefaultDomain { + domain = defaultDomain + } + + return domain +} diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image_test.go new file mode 100644 index 0000000000..fbe1668e8c --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image/docker_image_test.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + +package docker_image + +import "testing" + +func TestGetDomainFromDockerImage(t *testing.T) { + testCases := []struct { + name string + image string + expected string + }{ + { + name: "image with no domain", + image: "foo/bar", + expected: "docker.io", + }, + { + name: "image with docker.io domain", + image: "docker.io/foo/bar", + expected: "docker.io", + }, + { + name: "image with non-docker.io domain", + image: "test.xyz/foo/bar", + expected: "test.xyz", + }, + { + name: "single word image", + image: "ubuntu", + expected: "docker.io", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := GetDomainFromDockerImage(tc.image) + if result != tc.expected { + t.Errorf("GetDomainFromDockerImage(%q) = %q; want %q", tc.image, result, tc.expected) + } + }) + } +} diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go new file mode 100644 index 0000000000..2964985855 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go @@ -0,0 +1,287 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + +package secret_extraction + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// Constants for config +const ( + dockerConfigFileName = ".dockerconfigjson" + defaultSecretsPath = "/secrets" +) + +// Env variable names +const ( + envPodName = "POD_NAME" + envNameSpace = "NAMESPACE" +) + +type DockerConfigJSON struct { + Auths map[string]AuthEntry `json:"auths"` +} + +type AuthEntry struct { + Auth string `json:"auth,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type Credentials struct { + Username string + Password string +} + +type SecretManager struct { + k8sClient kubernetes.Interface + secretsPath string + namespace string + podName string + secretReader SecretReader + secretCreator SecretCreator +} + +type SecretReader interface { + ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) +} + +type SecretCreator interface { + CreateSecret(ctx context.Context, secret *v1.Secret) error +} + +type DefaultSecretReader struct{} + +type DefaultSecretCreator struct { + client kubernetes.Interface + namespace string +} + +func NewSecretManager() (*SecretManager, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("failed to create in-cluster config: %w", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create k8s client: %w", err) + } + + namespace := os.Getenv(envNameSpace) + if namespace == "" { + return nil, fmt.Errorf("environment variable %s is not set", envNameSpace) + } + + podName := os.Getenv(envPodName) + if podName == "" { + return nil, fmt.Errorf("environment variable %s is not set", envPodName) + } + + return &SecretManager{ + k8sClient: clientset, + secretsPath: defaultSecretsPath, + namespace: namespace, + podName: podName, + secretReader: &DefaultSecretReader{}, + secretCreator: &DefaultSecretCreator{client: clientset, namespace: namespace}, + }, nil +} + +// NewSecretManagerWithOptions creates a SecretManager with custom options (useful for testing) +func NewSecretManagerWithOptions(client kubernetes.Interface, secretsPath, namespace, podName string, reader SecretReader, creator SecretCreator) *SecretManager { + return &SecretManager{ + k8sClient: client, + secretsPath: secretsPath, + namespace: namespace, + podName: podName, + secretReader: reader, + secretCreator: creator, + } +} + +func (sm *SecretManager) CreateTemporarySecret(ctx context.Context, temporarySecretName, domain string) error { + if temporarySecretName == "" { + return fmt.Errorf("temporary secret name cannot be empty") + } + + if domain == "" { + return fmt.Errorf("domain cannot be empty") + } + + configs, err := sm.secretReader.ReadDockerConfigs(sm.secretsPath) + if err != nil { + return fmt.Errorf("failed to read Docker configs: %w", err) + } + + authEntry := sm.findAuthForDomain(domain, configs) + if authEntry == nil { + return fmt.Errorf("no authentication found for domain: %s", domain) + } + + creds, err := sm.extractCredentials(authEntry) + if err != nil { + return fmt.Errorf("failed to extract credentials for domain %s: %w", domain, err) + } + + secret, err := sm.buildSecret(ctx, temporarySecretName, creds) + if err != nil { + return fmt.Errorf("failed to build secret: %w", err) + } + + if err := sm.secretCreator.CreateSecret(ctx, secret); err != nil { + return fmt.Errorf("failed to create temporary secret: %w", err) + } + + return nil +} + +func (sm *SecretManager) findAuthForDomain(domain string, configs []DockerConfigJSON) *AuthEntry { + for _, config := range configs { + if auth, exists := config.Auths[domain]; exists { + return &auth + } + } + return nil +} + +func (sm *SecretManager) extractCredentials(auth *AuthEntry) (*Credentials, error) { + if auth == nil { + return nil, fmt.Errorf("auth entry is nil") + } + + if auth.Auth != "" { + decoded, err := base64.StdEncoding.DecodeString(auth.Auth) + if err != nil { + return nil, fmt.Errorf("failed to decode auth field: %w", err) + } + + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid auth format, expected username:password") + } + + return &Credentials{ + Username: base64.StdEncoding.EncodeToString([]byte(parts[0])), + Password: base64.StdEncoding.EncodeToString([]byte(parts[1])), + }, nil + } + + if auth.Username != "" && auth.Password != "" { + return &Credentials{ + Username: auth.Username, + Password: auth.Password, + }, nil + } + + return nil, fmt.Errorf("auth entry does not contain valid credentials") +} + +func (sm *SecretManager) buildSecret(ctx context.Context, secretName string, creds *Credentials) (*v1.Secret, error) { + pod, err := sm.k8sClient.CoreV1().Pods(sm.namespace).Get(ctx, sm.podName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get pod %s: %w", sm.podName, err) + } + + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: sm.namespace, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Pod", + Name: sm.podName, + UID: pod.UID, + }, + }, + }, + Data: map[string][]byte{ + "username": []byte(creds.Username), + "password": []byte(creds.Password), + }, + Type: v1.SecretTypeOpaque, + }, nil +} +func (r *DefaultSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { + var configs []DockerConfigJSON + var errors []error + + err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + errors = append(errors, fmt.Errorf("error accessing path %s, %w", path, err)) + return nil + } + + if info.IsDir() || filepath.Base(path) != dockerConfigFileName { + return nil + } + + config, err := r.readSingleConfig(path) + if err != nil { + errors = append(errors, fmt.Errorf("failed to read config from %s: %w", path, err)) + } + + configs = append(configs, *config) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk directory %s: %w", basePath, err) + } + + // Report non-fatal errors as warnings + for _, e := range errors { + fmt.Printf("Warning: %v\n", e) + } + + return configs, nil +} + +func (r *DefaultSecretReader) readSingleConfig(path string) (*DockerConfigJSON, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var config DockerConfigJSON + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + return &config, nil +} + +func (c *DefaultSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { + _, err := c.client.CoreV1().Secrets(c.namespace).Create(ctx, secret, metav1.CreateOptions{}) + return err +} + +func CreateTemporarySecret(temporarySecretName string, domain string) error { + manager, err := NewSecretManager() + if err != nil { + return err + } + + return manager.CreateTemporarySecret(context.Background(), temporarySecretName, domain) +} diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go new file mode 100644 index 0000000000..d26f2dc0e2 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + +package secret_extraction + +import ( + "context" + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" +) + +// Mock implementations +type mockSecretReader struct { + configs []DockerConfigJSON + err error +} + +func (m *mockSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { + return m.configs, m.err +} + +type mockSecretCreator struct { + createdSecrets []*v1.Secret + err error +} + +func (m *mockSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { + if m.err != nil { + return m.err + } + m.createdSecrets = append(m.createdSecrets, secret) + return nil +} + +func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { + // Create fake pod + fakePod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + UID: types.UID("test-uid"), + }, + } + + fakeClient := fake.NewSimpleClientset(fakePod) + + // Mock reader with auth field + mockReader := &mockSecretReader{ + configs: []DockerConfigJSON{ + { + Auths: map[string]AuthEntry{ + "registry.example.com": { + Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), + }, + }, + }, + }, + } + + mockCreator := &mockSecretCreator{} + + sm := NewSecretManagerWithOptions( + fakeClient, + "/secrets", + "test-namespace", + "test-pod", + mockReader, + mockCreator, + ) + + err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + // Verify secret was created + if len(mockCreator.createdSecrets) != 1 { + t.Fatalf("Expected 1 secret to be created, got: %d", len(mockCreator.createdSecrets)) + } + + secret := mockCreator.createdSecrets[0] + if secret.Name != "temp-secret" { + t.Errorf("Expected secret name 'temp-secret', got: %s", secret.Name) + } + + // Verify credentials are base64 encoded + expectedUsername := base64.StdEncoding.EncodeToString([]byte("user")) + expectedPassword := base64.StdEncoding.EncodeToString([]byte("pass")) + + if string(secret.Data["username"]) != expectedUsername { + t.Errorf("Expected username %s, got: %s", expectedUsername, string(secret.Data["username"])) + } + + if string(secret.Data["password"]) != expectedPassword { + t.Errorf("Expected password %s, got: %s", expectedPassword, string(secret.Data["password"])) + } +} + +func TestSecretManager_CreateTemporarySecret_WithUsernamePassword(t *testing.T) { + fakePod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + UID: types.UID("test-uid"), + }, + } + + fakeClient := fake.NewSimpleClientset(fakePod) + + // Mock reader with separate username/password fields + mockReader := &mockSecretReader{ + configs: []DockerConfigJSON{ + { + Auths: map[string]AuthEntry{ + "registry.example.com": { + Username: "testuser", + Password: "testpass", + }, + }, + }, + }, + } + + mockCreator := &mockSecretCreator{} + + sm := NewSecretManagerWithOptions( + fakeClient, + "/secrets", + "test-namespace", + "test-pod", + mockReader, + mockCreator, + ) + + err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + secret := mockCreator.createdSecrets[0] + if string(secret.Data["username"]) != "testuser" { + t.Errorf("Expected username 'testuser', got: %s", string(secret.Data["username"])) + } + + if string(secret.Data["password"]) != "testpass" { + t.Errorf("Expected password 'testpass', got: %s", string(secret.Data["password"])) + } +} + +func TestSecretManager_CreateTemporarySecret_DomainNotFound(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + + mockReader := &mockSecretReader{ + configs: []DockerConfigJSON{ + { + Auths: map[string]AuthEntry{ + "other-registry.com": { + Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), + }, + }, + }, + }, + } + + mockCreator := &mockSecretCreator{} + + sm := NewSecretManagerWithOptions( + fakeClient, + "/secrets", + "test-namespace", + "test-pod", + mockReader, + mockCreator, + ) + + err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") + + if err == nil { + t.Fatal("Expected error for domain not found") + } + + expectedError := "no authentication found for domain: registry.example.com" + if err.Error() != expectedError { + t.Errorf("Expected error '%s', got: '%s'", expectedError, err.Error()) + } +} + +func TestDefaultSecretReader_ReadDockerConfigs(t *testing.T) { + // Create temporary directory structure + tempDir, err := os.MkdirTemp("", "docker-config-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create test config + config := DockerConfigJSON{ + Auths: map[string]AuthEntry{ + "registry.example.com": { + Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), + }, + }, + } + + configData, err := json.Marshal(config) + if err != nil { + t.Fatalf("Failed to marshal config: %v", err) + } + + configPath := filepath.Join(tempDir, dockerConfigFileName) + if err := os.WriteFile(configPath, configData, 0644); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + reader := &DefaultSecretReader{} + configs, err := reader.ReadDockerConfigs(tempDir) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(configs) != 1 { + t.Fatalf("Expected 1 config, got: %d", len(configs)) + } + + if len(configs[0].Auths) != 1 { + t.Fatalf("Expected 1 auth entry, got: %d", len(configs[0].Auths)) + } + + auth, exists := configs[0].Auths["registry.example.com"] + if !exists { + t.Fatal("Expected auth entry for registry.example.com not found") + } + + expectedAuth := base64.StdEncoding.EncodeToString([]byte("user:pass")) + if auth.Auth != expectedAuth { + t.Errorf("Expected auth %s, got: %s", expectedAuth, auth.Auth) + } +} + +func TestExtractCredentials_AuthField(t *testing.T) { + sm := &SecretManager{} + + auth := &AuthEntry{ + Auth: base64.StdEncoding.EncodeToString([]byte("testuser:testpass")), + } + + creds, err := sm.extractCredentials(auth) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + expectedUsername := base64.StdEncoding.EncodeToString([]byte("testuser")) + expectedPassword := base64.StdEncoding.EncodeToString([]byte("testpass")) + + if creds.Username != expectedUsername { + t.Errorf("Expected username %s, got: %s", expectedUsername, creds.Username) + } + + if creds.Password != expectedPassword { + t.Errorf("Expected password %s, got: %s", expectedPassword, creds.Password) + } +} + +func TestExtractCredentials_SeparateFields(t *testing.T) { + sm := &SecretManager{} + + auth := &AuthEntry{ + Username: "testuser", + Password: "testpass", + } + + creds, err := sm.extractCredentials(auth) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if creds.Username != "testuser" { + t.Errorf("Expected username 'testuser', got: %s", creds.Username) + } + + if creds.Password != "testpass" { + t.Errorf("Expected password 'testpass', got: %s", creds.Password) + } +} diff --git a/auto-discovery/kubernetes/pull-secret-extractor/main.go b/auto-discovery/kubernetes/pull-secret-extractor/main.go new file mode 100644 index 0000000000..2db42ecb56 --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/main.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/secureCodeBox/auto-discovery/kubernetes/pull-secret-extractor/internal/docker_image" + secret_extraction "github.com/secureCodeBox/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction" +) + +const AppName = "pull-secret-extractor" + +type Config struct { + ImageID string + TemporarySecretName string +} + +func parseFlags() (*Config, error) { + config := &Config{} + + flag.StringVar(&config.ImageID, "imageID", "", "Docker image ID to extract domain from (required)") + flag.StringVar(&config.TemporarySecretName, "secret", "", "Name for the temporary secret (required)") + + flag.Parse() + + // If flags are not provided, use positional arguments + if config.ImageID == "" && config.TemporarySecretName == "" { + args := flag.Args() + if len(args) < 2 { + return nil, fmt.Errorf("usage: program OR use -imageID and -secret flags") + } + config.ImageID = args[0] + config.TemporarySecretName = args[1] + } + + if config.ImageID == "" { + return nil, fmt.Errorf("image ID is required (use -imageID flag or provide as first argument)") + } + + if config.TemporarySecretName == "" { + return nil, fmt.Errorf("temporary secret name is required (use -secret flag or provide as second argument)") + } + + return config, nil +} + +func run(config *Config) error { + domain := docker_image.GetDomainFromDockerImage(config.ImageID) + if domain == "" { + return fmt.Errorf("failed to extract domain from image ID: %s", config.ImageID) + } + + if err := secret_extraction.CreateTemporarySecret(config.TemporarySecretName, domain); err != nil { + return fmt.Errorf("failed to create temporary secret: %w", err) + } + + log.Printf("Successfully created temporary secret '%s' for domain '%s'", + config.TemporarySecretName, domain) + + return nil +} + +func main() { + log.SetPrefix(fmt.Sprintf("[%s] ", AppName)) + log.SetFlags(log.LstdFlags | log.Lshortfile) + + config, err := parseFlags() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) + flag.Usage() + os.Exit(1) + } + + if err := run(config); err != nil { + log.Printf("Application failed: %v", err) + os.Exit(1) + } +} diff --git a/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt b/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt deleted file mode 100644 index da7662dad2..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -cachetools==5.2.1 -certifi==2024.7.4 -charset-normalizer==3.0.1 -google-auth==2.16.0 -idna==3.7 -kubernetes==25.3.0 -oauthlib==3.2.2 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -python-dateutil==2.8.2 -PyYAML==6.0.1 -requests==2.32.4 -requests-oauthlib==1.3.1 -rsa==4.9 -six==1.16.0 -urllib3==2.5.0 -websocket-client==1.4.2 diff --git a/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt.license b/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt.license deleted file mode 100644 index 3034c0d74b..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/requirements.txt.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: the secureCodeBox authors - -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/auto-discovery/kubernetes/pull-secret-extractor/secret_extraction.py b/auto-discovery/kubernetes/pull-secret-extractor/secret_extraction.py deleted file mode 100644 index 1730a7dce5..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/secret_extraction.py +++ /dev/null @@ -1,128 +0,0 @@ -# SPDX-FileCopyrightText: the secureCodeBox authors -# -# SPDX-License-Identifier: Apache-2.0 - -import glob -import json -import sys -import base64 -import os - -from kubernetes import client, config - -from docker_image import get_domain_from_docker_image - - -def main(): - image_id = sys.argv[1] - temporary_secret_name = sys.argv[2] - - domain = get_domain_from_docker_image(image_id) - - raw_secrets = get_raw_secrets('/secrets') - correct_secret = get_correct_secret(domain, raw_secrets) - - if correct_secret: - username, password = get_user_and_password(correct_secret) - create_temporary_secret(username, password, temporary_secret_name) - print(f"Created temporary pull secret for domain: '{domain}'") - else: - print(f"No secrets found for domain: '{domain}'") - - -def get_raw_secrets(base_path: str): - """Reads in files called '.dockerconfigjson' in the path given and return the content of all files called so - :param base_path: Directory to search for dockerconfigjson files - :returns: List of secrets found in base_path - """ - raw_secrets = [] - for file_name in glob.glob(f'{base_path}/**/.dockerconfigjson', recursive=True): - with open(file_name) as file: - raw_secret = json.load(file) - raw_secrets.append(raw_secret) - return raw_secrets - - -def get_correct_secret(domain: str, secrets) -> dict[str, str]: - """Iterates over given list of secrets to find the secret that matches the URL in the given imageID - :param domain: The domain of the imageID of which the correct secret needs to be identified - :param secrets: List of secrets - :returns: Dict containing the secret matching the given imageID - """ - for secret in secrets: - for url, data in secret['auths'].items(): - if url == domain: - return data - - -def get_user_and_password(raw_secret: dict[str, str]) -> tuple[str, str]: - """Extracts username and password from a given secret - :param raw_secret: Dict containing the secret. Should contain key 'auth' (where username and password are - base64 encoded in a single line like: username:password), or 'username' and 'password' as a separate key - (also base64) - :returns: tuple containing username and password both base64 encoded - :raises KeyError: Structure of given secret does not contain expected structure. - """ - if 'auth' in raw_secret: - # secret is in form "username:password" (base64 encoded) - username_password_combo = decode_base64(raw_secret['auth']) - tmp_list = username_password_combo.split(":") - - # k8s wants the secrets as base64, so the individual values are converted back to base64 - username = encode_base64(tmp_list[0]) - password = encode_base64(tmp_list[1]) - return username, password - - elif 'username' in raw_secret and 'password' in raw_secret: - # username and password are already separated and base64 encoded, no need to do more - username = raw_secret['username'] - password = raw_secret['password'] - return username, password - - else: - raise KeyError('dockerconfigjson secret does not contain expected structure!') - - -def decode_base64(raw_string: str) -> str: - return base64.b64decode(raw_string).decode('utf-8') - - -def encode_base64(string: str) -> str: - return base64.b64encode(string.encode('utf-8')).decode('utf-8') - - -def create_temporary_secret(username: str, password: str, secret_name: str): - """Creates a secret with name 'secret_name' with 'username' and 'password' as data in given namespace. The secret has an ownerReference to the pod this container is running in. - :param username: base64 encoded string representing the desired value of the 'username' field in the secret - :param password: base64 encoded string representing the desired value of the 'password' field in the secret - :param secret_name: Name of the newly created secret - """ - config.load_incluster_config() - v1 = client.CoreV1Api() - - namespace = get_namespace() - - pod_name = get_pod_name() - pod = v1.read_namespaced_pod(name=pod_name, namespace=namespace) - - secret_data = {'username': username, 'password': password} - owner_references = client.V1OwnerReference(api_version='v1', name=pod_name, uid=pod.metadata.uid, kind='Pod') - metadata = client.V1ObjectMeta(name=secret_name, namespace=namespace, owner_references=[owner_references]) - secret_body = client.V1Secret(api_version='v1', kind='Secret', metadata=metadata, data=secret_data, type='Opaque') - v1.create_namespaced_secret(namespace=namespace, body=secret_body) - - -def get_pod_name() -> str: - """Read pod name from environment variable called 'POD_NAME'. - Should be set like this: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/""" - return os.environ['POD_NAME'] - - -def get_namespace() -> str: - """Read pod name from environment variable called 'NAMESPACE'. - Should be set like this: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/""" - return os.environ['NAMESPACE'] - - -if __name__ == '__main__': - main() diff --git a/auto-discovery/kubernetes/pull-secret-extractor/integration-test/test-pod.sh b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh similarity index 93% rename from auto-discovery/kubernetes/pull-secret-extractor/integration-test/test-pod.sh rename to auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh index f72670cbb4..a2c304a094 100755 --- a/auto-discovery/kubernetes/pull-secret-extractor/integration-test/test-pod.sh +++ b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh @@ -22,7 +22,7 @@ metadata: namespace: integration-test --- apiVersion: v1 -kind: Pod +kind: Pod metadata: name: init-container-test namespace: integration-test @@ -35,8 +35,7 @@ spec: initContainers: - name: init-container-test-container image: $1 - command: ["python"] - args: ["secret_extraction.py", "fake-registry.xyz/ubuntu:32131", "test-secret", "default"] + args: ["fake-registry.xyz/ubuntu:32131", "test-secret", "default"] volumeMounts: - name: regcred-volume mountPath: "/secrets/regcred" diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/.dockerconfigjson b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/.dockerconfigjson similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/.dockerconfigjson rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/.dockerconfigjson diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/.dockerconfigjson.license b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/.dockerconfigjson.license similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/.dockerconfigjson.license rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/.dockerconfigjson.license diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/not_a_docker_config_json b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/not_a_docker_config_json similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/not_a_docker_config_json rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/not_a_docker_config_json diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/not_a_docker_config_json.license b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/not_a_docker_config_json.license similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_1/not_a_docker_config_json.license rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_1/not_a_docker_config_json.license diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_2/.dockerconfigjson b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_2/.dockerconfigjson similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_2/.dockerconfigjson rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_2/.dockerconfigjson diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_2/.dockerconfigjson.license b/auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_2/.dockerconfigjson.license similarity index 100% rename from auto-discovery/kubernetes/pull-secret-extractor/test_secrets/secret_2/.dockerconfigjson.license rename to auto-discovery/kubernetes/pull-secret-extractor/test/testdata/secrets/secret_2/.dockerconfigjson.license diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_docker_image.py b/auto-discovery/kubernetes/pull-secret-extractor/test_docker_image.py deleted file mode 100644 index 945c84dd96..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/test_docker_image.py +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-FileCopyrightText: the secureCodeBox authors -# -# SPDX-License-Identifier: Apache-2.0 - -from unittest import TestCase - -from docker_image import get_domain_from_docker_image - - -class Test(TestCase): - def test_get_domain_from_docker_image_with_no_domain(self): - test_image = "foo/bar" - domain = get_domain_from_docker_image(test_image) - self.assertEqual("docker.io", domain) - - def test_get_domain_from_docker_image_with_dockerio_domain(self): - test_image = "docker.io/foo/bar" - domain = get_domain_from_docker_image(test_image) - self.assertEqual("docker.io", domain) - - def test_get_domain_from_docker_image_with_non_dockerio_domain(self): - test_image = "test.xyz/foo/bar" - domain = get_domain_from_docker_image(test_image) - self.assertEqual("test.xyz", domain) - - def test_get_domain_from_docker_image_with_single_world_image(self): - test_image = "ubuntu" - domain = get_domain_from_docker_image(test_image) - self.assertEqual("docker.io", domain) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test_secret_extraction.py b/auto-discovery/kubernetes/pull-secret-extractor/test_secret_extraction.py deleted file mode 100644 index a0a6191eb9..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/test_secret_extraction.py +++ /dev/null @@ -1,65 +0,0 @@ -# SPDX-FileCopyrightText: the secureCodeBox authors -# -# SPDX-License-Identifier: Apache-2.0 - -import sys -import unittest - -from unittest.mock import MagicMock - -# mock kubernetes import so it doesnt need to be installed to run these tests -sys.modules['kubernetes'] = MagicMock() -from secret_extraction import * - - -class MyTestCase(unittest.TestCase): - - def test_get_raw_secrets(self): - actual = get_raw_secrets('test_secrets') - - with open('test_secrets/secret_1/.dockerconfigjson') as file: - expected_secret_1 = json.load(file) - - with open('test_secrets/secret_2/.dockerconfigjson') as file: - expected_secret_2 = json.load(file) - - # for some reason assertCountEqual doesnt work here - self.assertIn(expected_secret_1, actual) - self.assertIn(expected_secret_2, actual) - - def test_get_correct_secret(self): - with open('test_secrets/secret_1/.dockerconfigjson') as file: - secret_list = [json.load(file)] - - with open('test_secrets/secret_2/.dockerconfigjson') as file: - secret_list.append(json.load(file)) - - actual = get_correct_secret('localhost:5000', secret_list) - - # testuser:testpassword base64 encoded - expected = {'auth': 'dGVzdHVzZXI6dGVzdHBhc3N3b3Jk'} - - self.assertCountEqual(expected, actual) - - def test_get_user_and_password_given_auth_string(self): - secret = {'auth': 'dGVzdHVzZXI6dGVzdHBhc3N3b3Jk'} - actual = get_user_and_password(secret) - - # testuser, testpassword base64 encoded - expected = ('dGVzdHVzZXI=', 'dGVzdHBhc3N3b3Jk') - - self.assertEqual(expected, actual) - - def test_get_and_password_given_username_and_password_as_separate_string(self): - secret = { - 'username': 'dGVzdHVzZXI=', - 'password': 'dGVzdHBhc3N3b3Jk' - } - actual = get_user_and_password(secret) - - expected = (secret['username'], secret['password']) - self.assertEqual(expected, actual) - - -if __name__ == '__main__': - unittest.main() From ad2819ff2449bc34764b52de15cd8e371dca6ba9 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Tue, 9 Sep 2025 08:51:34 +0200 Subject: [PATCH 02/10] Update secret_extraction_test Signed-off-by: Patrick Weiss --- .../secret_extraction_test.go | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go index d26f2dc0e2..75f34c3671 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -19,21 +19,21 @@ import ( ) // Mock implementations -type mockSecretReader struct { +type MockSecretReader struct { configs []DockerConfigJSON err error } -func (m *mockSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { +func (m *MockSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { return m.configs, m.err } -type mockSecretCreator struct { +type MockSecretCreator struct { createdSecrets []*v1.Secret err error } -func (m *mockSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { +func (m *MockSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { if m.err != nil { return m.err } @@ -42,7 +42,6 @@ func (m *mockSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) } func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { - // Create fake pod fakePod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", @@ -53,8 +52,7 @@ func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { fakeClient := fake.NewSimpleClientset(fakePod) - // Mock reader with auth field - mockReader := &mockSecretReader{ + mockReader := &MockSecretReader{ configs: []DockerConfigJSON{ { Auths: map[string]AuthEntry{ @@ -66,7 +64,7 @@ func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { }, } - mockCreator := &mockSecretCreator{} + mockCreator := &MockSecretCreator{} sm := NewSecretManagerWithOptions( fakeClient, @@ -83,7 +81,6 @@ func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { t.Fatalf("Expected no error, got: %v", err) } - // Verify secret was created if len(mockCreator.createdSecrets) != 1 { t.Fatalf("Expected 1 secret to be created, got: %d", len(mockCreator.createdSecrets)) } @@ -93,7 +90,6 @@ func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { t.Errorf("Expected secret name 'temp-secret', got: %s", secret.Name) } - // Verify credentials are base64 encoded expectedUsername := base64.StdEncoding.EncodeToString([]byte("user")) expectedPassword := base64.StdEncoding.EncodeToString([]byte("pass")) @@ -118,7 +114,7 @@ func TestSecretManager_CreateTemporarySecret_WithUsernamePassword(t *testing.T) fakeClient := fake.NewSimpleClientset(fakePod) // Mock reader with separate username/password fields - mockReader := &mockSecretReader{ + mockReader := &MockSecretReader{ configs: []DockerConfigJSON{ { Auths: map[string]AuthEntry{ @@ -131,7 +127,7 @@ func TestSecretManager_CreateTemporarySecret_WithUsernamePassword(t *testing.T) }, } - mockCreator := &mockSecretCreator{} + mockCreator := &MockSecretCreator{} sm := NewSecretManagerWithOptions( fakeClient, @@ -161,7 +157,7 @@ func TestSecretManager_CreateTemporarySecret_WithUsernamePassword(t *testing.T) func TestSecretManager_CreateTemporarySecret_DomainNotFound(t *testing.T) { fakeClient := fake.NewSimpleClientset() - mockReader := &mockSecretReader{ + mockReader := &MockSecretReader{ configs: []DockerConfigJSON{ { Auths: map[string]AuthEntry{ @@ -173,7 +169,7 @@ func TestSecretManager_CreateTemporarySecret_DomainNotFound(t *testing.T) { }, } - mockCreator := &mockSecretCreator{} + mockCreator := &MockSecretCreator{} sm := NewSecretManagerWithOptions( fakeClient, @@ -197,14 +193,12 @@ func TestSecretManager_CreateTemporarySecret_DomainNotFound(t *testing.T) { } func TestDefaultSecretReader_ReadDockerConfigs(t *testing.T) { - // Create temporary directory structure tempDir, err := os.MkdirTemp("", "docker-config-test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) - // Create test config config := DockerConfigJSON{ Auths: map[string]AuthEntry{ "registry.example.com": { From e1aeb73517659757b661be6b2a8e8c9977400ad3 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Tue, 9 Sep 2025 09:22:41 +0200 Subject: [PATCH 03/10] add missing licensing information Signed-off-by: Patrick Weiss --- auto-discovery/kubernetes/pull-secret-extractor/go.mod | 4 ++++ auto-discovery/kubernetes/pull-secret-extractor/go.sum | 4 ++++ .../kubernetes/pull-secret-extractor/go.sum.license | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 auto-discovery/kubernetes/pull-secret-extractor/go.sum.license diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.mod b/auto-discovery/kubernetes/pull-secret-extractor/go.mod index 09b2ff99bb..db9d8f6dd7 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/go.mod +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.mod @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + module github.com/secureCodeBox/auto-discovery/kubernetes/pull-secret-extractor go 1.24.5 diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.sum b/auto-discovery/kubernetes/pull-secret-extractor/go.sum index 1ca7012133..a2c38017f3 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/go.sum +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.sum @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.sum.license b/auto-discovery/kubernetes/pull-secret-extractor/go.sum.license new file mode 100644 index 0000000000..3034c0d74b --- /dev/null +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.sum.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: the secureCodeBox authors + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file From 739198b6dbe66cdb7ab28acd23d1e652c74b4e76 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Tue, 9 Sep 2025 09:25:50 +0200 Subject: [PATCH 04/10] add task to ci job Signed-off-by: Patrick Weiss --- .github/workflows/ci.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b7d89d9f58..68872230f2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -283,6 +283,16 @@ jobs: go fmt ./... go vet ./... + - name: Download Task + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: task + path: ./task + + - name: Make Task globally available + run: | + chmod +x ./task/task && sudo mv ./task/task /usr/local/bin/task + - name: Download Kind uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: From 67f161490211845f2e3cc5c3608ac7b558f451d5 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Tue, 9 Sep 2025 09:46:51 +0200 Subject: [PATCH 05/10] fix go.sum Signed-off-by: Patrick Weiss --- auto-discovery/kubernetes/pull-secret-extractor/go.sum | 4 ---- 1 file changed, 4 deletions(-) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.sum b/auto-discovery/kubernetes/pull-secret-extractor/go.sum index a2c38017f3..1ca7012133 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/go.sum +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.sum @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: the secureCodeBox authors -// -// SPDX-License-Identifier: Apache-2.0 - github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= From e462da3f06101d1773dfa77e14bd766472b208e1 Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Thu, 11 Sep 2025 09:39:25 +0200 Subject: [PATCH 06/10] implement feedback Signed-off-by: Patrick Weiss --- .github/workflows/ci.yaml | 4 +- .../controllers/container_scan_controller.go | 3 +- .../kubernetes/pull-secret-extractor/Makefile | 76 ----- .../pull-secret-extractor/Taskfile.yaml | 7 - .../kubernetes/pull-secret-extractor/go.mod | 30 +- .../kubernetes/pull-secret-extractor/go.sum | 89 +++-- .../secret_extraction/secret_extraction.go | 221 +++++-------- .../secret_extraction_test.go | 308 ++++++------------ .../kubernetes/pull-secret-extractor/main.go | 12 +- .../test/integration/test-pod.sh | 2 +- 10 files changed, 261 insertions(+), 491 deletions(-) delete mode 100644 auto-discovery/kubernetes/pull-secret-extractor/Makefile diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 68872230f2..61af421a31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -272,8 +272,8 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Setup Python Version - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - name: Go Setup + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v6.0.0 with: go-version-file: "auto-discovery/kubernetes/go.mod" diff --git a/auto-discovery/kubernetes/controllers/container_scan_controller.go b/auto-discovery/kubernetes/controllers/container_scan_controller.go index 736a5f4841..b5915caeab 100644 --- a/auto-discovery/kubernetes/controllers/container_scan_controller.go +++ b/auto-discovery/kubernetes/controllers/container_scan_controller.go @@ -335,8 +335,7 @@ func getSecretExtractionInitContainer(imageID string, scanConfig config.ScanConf return corev1.Container{ Name: "secret-extraction-to-env", Image: "docker.io/securecodebox/auto-discovery-pull-secret-extractor", - Command: []string{"python"}, - Args: []string{"secret_extraction.py", imageID, temporarySecretName}, + Args: []string{"-imageID", imageID, "-secret", temporarySecretName}, VolumeMounts: volumeMounts, Env: []corev1.EnvVar{ { diff --git a/auto-discovery/kubernetes/pull-secret-extractor/Makefile b/auto-discovery/kubernetes/pull-secret-extractor/Makefile deleted file mode 100644 index a5bb390cf0..0000000000 --- a/auto-discovery/kubernetes/pull-secret-extractor/Makefile +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-FileCopyrightText: the secureCodeBox authors -# -# SPDX-License-Identifier: Apache-2.0 - -include ../../../prerequisites.mk - -IMG_NS ?= securecodebox - -# Image URL to use all building/pushing image targets -IMG ?= auto-discovery-secret-extractor - -# Tag used for the image -IMG_TAG ?= sha-$$(git rev-parse --short HEAD) - - -##@ General - -# The help target prints out all targets with their descriptions organized -# beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk commands is responsible for reading the -# entire set of makefiles included in this invocation, looking for lines of the -# file as xyz: ## something, and then pretty-format the target and help. Then, -# if there's a line with ##@ something, that gets pretty-printed as a category. -# More info on the usage of ANSI control characters for terminal formatting: -# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters -# More info on the awk command: -# http://linuxcommand.org/lc3_adv_awk.php - -.PHONY: help -help: ## Display this help. - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - - -##@ Development - -.PHONY: test -test: unit-test integration-test - -.PHONY: unit-test -unit-test: - go test ./... - -.PHONY: integration-test -integration-test: docker-build docker-export kind-import - @echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'." - kubectl delete namespace integration-test --wait || true - kubectl create namespace integration-test - - ./integration-test/test-pod.sh ${IMG_NS}/${IMG}:${IMG_TAG} - kubectl wait --for=condition=ready --timeout=60s -n integration-test pod/init-container-test - - kubectl get secret --namespace integration-test test-secret - -##@ Build - - -.PHONY: docker-build -docker-build: ## Build docker image with the manager. - @echo ".: ⚙️ Build Container Images" - docker build -t ${IMG_NS}/${IMG}:${IMG_TAG} . - -.PHONY: docker-push -docker-push: ## Push docker image with the manager. - docker push ${IMG_NS}/${IMG}:${IMG_TAG} - -.PHONY: docker-export -docker-export: - @echo ".: 💾 Export Container Images" - docker save $(IMG_NS)/$(IMG):$(IMG_TAG) > $(IMG).tar - -##@ Deployment - -.PHONY: kind-import -kind-import: - @echo ".: 💾 Importing the image archive to local kind cluster." - kind load image-archive ./$(IMG).tar diff --git a/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml b/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml index 97d2dce7d7..32e0f12a7d 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml +++ b/auto-discovery/kubernetes/pull-secret-extractor/Taskfile.yaml @@ -43,13 +43,6 @@ tasks: - 'echo "⚙️ Build Container Images"' - docker build -t {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} . - docker-push: - desc: Push docker image to registry - deps: - - docker-build - cmds: - - docker push {{.IMG_NS}}/{{.IMG}}:{{.IMG_TAG}} - docker-export: desc: Export container image to tar archive deps: diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.mod b/auto-discovery/kubernetes/pull-secret-extractor/go.mod index db9d8f6dd7..8151c1bdc7 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/go.mod +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.mod @@ -7,31 +7,34 @@ module github.com/secureCodeBox/auto-discovery/kubernetes/pull-secret-extractor go 1.24.5 require ( - k8s.io/api v0.33.4 - k8s.io/apimachinery v0.33.4 - k8s.io/client-go v0.33.4 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 + sigs.k8s.io/controller-runtime v0.22.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sys v0.31.0 // indirect @@ -42,11 +45,12 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/client-go v0.34.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/go.sum b/auto-discovery/kubernetes/pull-secret-extractor/go.sum index 1ca7012133..7f305f9954 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/go.sum +++ b/auto-discovery/kubernetes/pull-secret-extractor/go.sum @@ -1,13 +1,21 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +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/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= @@ -20,9 +28,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -48,22 +55,31 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -79,6 +95,14 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -130,24 +154,27 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= -k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= -k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= -k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= -k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= -k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go index 2964985855..09e08eccbd 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go @@ -16,8 +16,9 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" ) // Constants for config @@ -47,110 +48,93 @@ type Credentials struct { Password string } -type SecretManager struct { - k8sClient kubernetes.Interface - secretsPath string - namespace string - podName string - secretReader SecretReader - secretCreator SecretCreator -} - -type SecretReader interface { - ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) -} - -type SecretCreator interface { - CreateSecret(ctx context.Context, secret *v1.Secret) error -} - -type DefaultSecretReader struct{} +func CreateTemporarySecret(ctx context.Context, k8sClient client.Client, temporarySecretName, domain, namespace, podName, secretsPath string) error { + if temporarySecretName == "" { + return fmt.Errorf("temporary secret name cannot be empty") + } -type DefaultSecretCreator struct { - client kubernetes.Interface - namespace string -} + if domain == "" { + return fmt.Errorf("domain cannot be empty") + } -func NewSecretManager() (*SecretManager, error) { - config, err := rest.InClusterConfig() + configs, err := readDockerConfigs(secretsPath) if err != nil { - return nil, fmt.Errorf("failed to create in-cluster config: %w", err) + return fmt.Errorf("failed to read Docker configs: %w", err) } - clientset, err := kubernetes.NewForConfig(config) + authEntry := findAuthForDomain(domain, configs) + if authEntry == nil { + return fmt.Errorf("no authentication found for domain: %s", domain) + } + + creds, err := extractCredentials(authEntry) if err != nil { - return nil, fmt.Errorf("failed to create k8s client: %w", err) + return fmt.Errorf("failed to extract credentials for domain %s: %w", domain, err) } - namespace := os.Getenv(envNameSpace) - if namespace == "" { - return nil, fmt.Errorf("environment variable %s is not set", envNameSpace) + secret, err := buildSecret(ctx, k8sClient, temporarySecretName, namespace, podName, creds) + if err != nil { + return fmt.Errorf("failed to build secret: %w", err) } - podName := os.Getenv(envPodName) - if podName == "" { - return nil, fmt.Errorf("environment variable %s is not set", envPodName) + if err := k8sClient.Create(ctx, secret); err != nil { + return fmt.Errorf("failed to create temporary secret: %w", err) } - return &SecretManager{ - k8sClient: clientset, - secretsPath: defaultSecretsPath, - namespace: namespace, - podName: podName, - secretReader: &DefaultSecretReader{}, - secretCreator: &DefaultSecretCreator{client: clientset, namespace: namespace}, - }, nil + return nil } -// NewSecretManagerWithOptions creates a SecretManager with custom options (useful for testing) -func NewSecretManagerWithOptions(client kubernetes.Interface, secretsPath, namespace, podName string, reader SecretReader, creator SecretCreator) *SecretManager { - return &SecretManager{ - k8sClient: client, - secretsPath: secretsPath, - namespace: namespace, - podName: podName, - secretReader: reader, - secretCreator: creator, - } -} +func readDockerConfigs(basePath string) ([]DockerConfigJSON, error) { + var configs []DockerConfigJSON -func (sm *SecretManager) CreateTemporarySecret(ctx context.Context, temporarySecretName, domain string) error { - if temporarySecretName == "" { - return fmt.Errorf("temporary secret name cannot be empty") - } + err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Printf("Warning: error accessing path %s: %v\n", path, err) + return nil + } - if domain == "" { - return fmt.Errorf("domain cannot be empty") - } + if info.IsDir() || filepath.Base(path) != dockerConfigFileName { + return nil + } + + config, err := readSingleConfig(path) + if err != nil { + fmt.Printf("Warning: failed to read config from %s: %v\n", path, err) + return nil + } + + configs = append(configs, *config) + return nil + }) - configs, err := sm.secretReader.ReadDockerConfigs(sm.secretsPath) if err != nil { - return fmt.Errorf("failed to read Docker configs: %w", err) + return nil, fmt.Errorf("failed to walk directory %s: %w", basePath, err) } - authEntry := sm.findAuthForDomain(domain, configs) - if authEntry == nil { - return fmt.Errorf("no authentication found for domain: %s", domain) - } + return configs, nil +} - creds, err := sm.extractCredentials(authEntry) +func readSingleConfig(path string) (*DockerConfigJSON, error) { + file, err := os.Open(path) if err != nil { - return fmt.Errorf("failed to extract credentials for domain %s: %w", domain, err) + return nil, fmt.Errorf("failed to open file: %w", err) } + defer file.Close() - secret, err := sm.buildSecret(ctx, temporarySecretName, creds) + data, err := io.ReadAll(file) if err != nil { - return fmt.Errorf("failed to build secret: %w", err) + return nil, fmt.Errorf("failed to read file: %w", err) } - if err := sm.secretCreator.CreateSecret(ctx, secret); err != nil { - return fmt.Errorf("failed to create temporary secret: %w", err) + var config DockerConfigJSON + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) } - return nil + return &config, nil } -func (sm *SecretManager) findAuthForDomain(domain string, configs []DockerConfigJSON) *AuthEntry { +func findAuthForDomain(domain string, configs []DockerConfigJSON) *AuthEntry { for _, config := range configs { if auth, exists := config.Auths[domain]; exists { return &auth @@ -159,7 +143,7 @@ func (sm *SecretManager) findAuthForDomain(domain string, configs []DockerConfig return nil } -func (sm *SecretManager) extractCredentials(auth *AuthEntry) (*Credentials, error) { +func extractCredentials(auth *AuthEntry) (*Credentials, error) { if auth == nil { return nil, fmt.Errorf("auth entry is nil") } @@ -191,21 +175,21 @@ func (sm *SecretManager) extractCredentials(auth *AuthEntry) (*Credentials, erro return nil, fmt.Errorf("auth entry does not contain valid credentials") } -func (sm *SecretManager) buildSecret(ctx context.Context, secretName string, creds *Credentials) (*v1.Secret, error) { - pod, err := sm.k8sClient.CoreV1().Pods(sm.namespace).Get(ctx, sm.podName, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to get pod %s: %w", sm.podName, err) +func buildSecret(ctx context.Context, k8sClient client.Client, secretName, namespace, podName string, creds *Credentials) (*v1.Secret, error) { + pod := &v1.Pod{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: podName, Namespace: namespace}, pod); err != nil { + return nil, fmt.Errorf("failed to get pod %s: %w", podName, err) } return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, - Namespace: sm.namespace, + Namespace: namespace, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "Pod", - Name: sm.podName, + Name: podName, UID: pod.UID, }, }, @@ -217,71 +201,42 @@ func (sm *SecretManager) buildSecret(ctx context.Context, secretName string, cre Type: v1.SecretTypeOpaque, }, nil } -func (r *DefaultSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { - var configs []DockerConfigJSON - var errors []error - - err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { - if err != nil { - errors = append(errors, fmt.Errorf("error accessing path %s, %w", path, err)) - return nil - } - - if info.IsDir() || filepath.Base(path) != dockerConfigFileName { - return nil - } - - config, err := r.readSingleConfig(path) - if err != nil { - errors = append(errors, fmt.Errorf("failed to read config from %s: %w", path, err)) - } - - configs = append(configs, *config) - return nil - }) +func createK8sClient() (client.Client, error) { + cfg, err := config.GetConfig() if err != nil { - return nil, fmt.Errorf("failed to walk directory %s: %w", basePath, err) + return nil, fmt.Errorf("failed to get Kubernetes config: %w", err) } - // Report non-fatal errors as warnings - for _, e := range errors { - fmt.Printf("Warning: %v\n", e) + scheme := runtime.NewScheme() + if err := v1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to add core v1 to scheme: %w", err) } - return configs, nil -} - -func (r *DefaultSecretReader) readSingleConfig(path string) (*DockerConfigJSON, error) { - file, err := os.Open(path) + k8sClient, err := client.New(cfg, client.Options{Scheme: scheme}) if err != nil { - return nil, fmt.Errorf("failed to open file: %w", err) + return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) } - defer file.Close() - data, err := io.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } + return k8sClient, nil +} - var config DockerConfigJSON - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) +func CreateTemporarySecretFromEnv(temporarySecretName, domain string) error { + namespace := os.Getenv(envNameSpace) + if namespace == "" { + return fmt.Errorf("environment variable %s is not set", envNameSpace) } - return &config, nil -} - -func (c *DefaultSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { - _, err := c.client.CoreV1().Secrets(c.namespace).Create(ctx, secret, metav1.CreateOptions{}) - return err -} + podName := os.Getenv(envPodName) + if podName == "" { + return fmt.Errorf("environment variable %s is not set", envPodName) + } -func CreateTemporarySecret(temporarySecretName string, domain string) error { - manager, err := NewSecretManager() + k8sClient, err := createK8sClient() if err != nil { - return err + return fmt.Errorf("failed to create Kubernetes client: %w", err) } - return manager.CreateTemporarySecret(context.Background(), temporarySecretName, domain) + ctx := context.Background() + return CreateTemporarySecret(ctx, k8sClient, temporarySecretName, domain, namespace, podName, defaultSecretsPath) } diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go index 75f34c3671..c4e8cebde0 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: the secureCodeBox authors -// -// SPDX-License-Identifier: Apache-2.0 - package secret_extraction import ( @@ -14,279 +10,161 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -// Mock implementations -type MockSecretReader struct { - configs []DockerConfigJSON - err error -} - -func (m *MockSecretReader) ReadDockerConfigs(basePath string) ([]DockerConfigJSON, error) { - return m.configs, m.err -} - -type MockSecretCreator struct { - createdSecrets []*v1.Secret - err error -} - -func (m *MockSecretCreator) CreateSecret(ctx context.Context, secret *v1.Secret) error { - if m.err != nil { - return m.err - } - m.createdSecrets = append(m.createdSecrets, secret) - return nil -} +func TestCreateTemporarySecret(t *testing.T) { + scheme := runtime.NewScheme() + v1.AddToScheme(scheme) -func TestSecretManager_CreateTemporarySecret_Success(t *testing.T) { - fakePod := &v1.Pod{ + pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "test-namespace", - UID: types.UID("test-uid"), + UID: types.UID("test-uid-123"), }, } - fakeClient := fake.NewSimpleClientset(fakePod) + k8sClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod). + Build() - mockReader := &MockSecretReader{ - configs: []DockerConfigJSON{ - { - Auths: map[string]AuthEntry{ - "registry.example.com": { - Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), - }, - }, + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, dockerConfigFileName) + + config := DockerConfigJSON{ + Auths: map[string]AuthEntry{ + "example.com": { + Auth: base64.StdEncoding.EncodeToString([]byte("testuser:testpass")), }, }, } - mockCreator := &MockSecretCreator{} - - sm := NewSecretManagerWithOptions( - fakeClient, - "/secrets", - "test-namespace", - "test-pod", - mockReader, - mockCreator, - ) + configData, _ := json.Marshal(config) + os.WriteFile(configPath, configData, 0644) - err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") + ctx := context.Background() + err := CreateTemporarySecret(ctx, k8sClient, "test-secret", "example.com", "test-namespace", "test-pod", tempDir) if err != nil { t.Fatalf("Expected no error, got: %v", err) } - if len(mockCreator.createdSecrets) != 1 { - t.Fatalf("Expected 1 secret to be created, got: %d", len(mockCreator.createdSecrets)) - } - - secret := mockCreator.createdSecrets[0] - if secret.Name != "temp-secret" { - t.Errorf("Expected secret name 'temp-secret', got: %s", secret.Name) + secret := &v1.Secret{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: "test-secret", Namespace: "test-namespace"}, secret) + if err != nil { + t.Fatalf("Secret was not created: %v", err) } - expectedUsername := base64.StdEncoding.EncodeToString([]byte("user")) - expectedPassword := base64.StdEncoding.EncodeToString([]byte("pass")) + expectedUsername := base64.StdEncoding.EncodeToString([]byte("testuser")) + expectedPassword := base64.StdEncoding.EncodeToString([]byte("testpass")) if string(secret.Data["username"]) != expectedUsername { - t.Errorf("Expected username %s, got: %s", expectedUsername, string(secret.Data["username"])) + t.Errorf("Expected username %s, got %s", expectedUsername, string(secret.Data["username"])) } if string(secret.Data["password"]) != expectedPassword { - t.Errorf("Expected password %s, got: %s", expectedPassword, string(secret.Data["password"])) - } -} - -func TestSecretManager_CreateTemporarySecret_WithUsernamePassword(t *testing.T) { - fakePod := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-pod", - Namespace: "test-namespace", - UID: types.UID("test-uid"), - }, + t.Errorf("Expected password %s, got %s", expectedPassword, string(secret.Data["password"])) } - fakeClient := fake.NewSimpleClientset(fakePod) - - // Mock reader with separate username/password fields - mockReader := &MockSecretReader{ - configs: []DockerConfigJSON{ - { - Auths: map[string]AuthEntry{ - "registry.example.com": { - Username: "testuser", - Password: "testpass", - }, - }, - }, - }, + if len(secret.OwnerReferences) != 1 { + t.Fatalf("Expected 1 owner reference, got %d", len(secret.OwnerReferences)) } - mockCreator := &MockSecretCreator{} - - sm := NewSecretManagerWithOptions( - fakeClient, - "/secrets", - "test-namespace", - "test-pod", - mockReader, - mockCreator, - ) - - err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - secret := mockCreator.createdSecrets[0] - if string(secret.Data["username"]) != "testuser" { - t.Errorf("Expected username 'testuser', got: %s", string(secret.Data["username"])) - } - - if string(secret.Data["password"]) != "testpass" { - t.Errorf("Expected password 'testpass', got: %s", string(secret.Data["password"])) + ownerRef := secret.OwnerReferences[0] + if ownerRef.Name != "test-pod" || ownerRef.UID != "test-uid-123" { + t.Errorf("Owner reference not set correctly: %+v", ownerRef) } } -func TestSecretManager_CreateTemporarySecret_DomainNotFound(t *testing.T) { - fakeClient := fake.NewSimpleClientset() - - mockReader := &MockSecretReader{ - configs: []DockerConfigJSON{ - { - Auths: map[string]AuthEntry{ - "other-registry.com": { - Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), - }, - }, - }, - }, - } - - mockCreator := &MockSecretCreator{} - - sm := NewSecretManagerWithOptions( - fakeClient, - "/secrets", - "test-namespace", - "test-pod", - mockReader, - mockCreator, - ) - - err := sm.CreateTemporarySecret(context.Background(), "temp-secret", "registry.example.com") - - if err == nil { - t.Fatal("Expected error for domain not found") - } - - expectedError := "no authentication found for domain: registry.example.com" - if err.Error() != expectedError { - t.Errorf("Expected error '%s', got: '%s'", expectedError, err.Error()) - } -} - -func TestDefaultSecretReader_ReadDockerConfigs(t *testing.T) { - tempDir, err := os.MkdirTemp("", "docker-config-test") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) +func TestReadDockerConfigs(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, dockerConfigFileName) config := DockerConfigJSON{ Auths: map[string]AuthEntry{ "registry.example.com": { - Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), + Username: "testuser", + Password: "testpass", }, }, } - configData, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - configPath := filepath.Join(tempDir, dockerConfigFileName) - if err := os.WriteFile(configPath, configData, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - reader := &DefaultSecretReader{} - configs, err := reader.ReadDockerConfigs(tempDir) + configData, _ := json.Marshal(config) + os.WriteFile(configPath, configData, 0644) + configs, err := readDockerConfigs(tempDir) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if len(configs) != 1 { - t.Fatalf("Expected 1 config, got: %d", len(configs)) - } - - if len(configs[0].Auths) != 1 { - t.Fatalf("Expected 1 auth entry, got: %d", len(configs[0].Auths)) + t.Fatalf("Expected 1 config, got %d", len(configs)) } auth, exists := configs[0].Auths["registry.example.com"] if !exists { - t.Fatal("Expected auth entry for registry.example.com not found") + t.Fatal("Expected auth entry for registry.example.com") } - expectedAuth := base64.StdEncoding.EncodeToString([]byte("user:pass")) - if auth.Auth != expectedAuth { - t.Errorf("Expected auth %s, got: %s", expectedAuth, auth.Auth) + if auth.Username != "testuser" || auth.Password != "testpass" { + t.Errorf("Expected testuser/testpass, got %s/%s", auth.Username, auth.Password) } } -func TestExtractCredentials_AuthField(t *testing.T) { - sm := &SecretManager{} - - auth := &AuthEntry{ - Auth: base64.StdEncoding.EncodeToString([]byte("testuser:testpass")), - } - - creds, err := sm.extractCredentials(auth) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - expectedUsername := base64.StdEncoding.EncodeToString([]byte("testuser")) - expectedPassword := base64.StdEncoding.EncodeToString([]byte("testpass")) - - if creds.Username != expectedUsername { - t.Errorf("Expected username %s, got: %s", expectedUsername, creds.Username) - } - - if creds.Password != expectedPassword { - t.Errorf("Expected password %s, got: %s", expectedPassword, creds.Password) - } -} - -func TestExtractCredentials_SeparateFields(t *testing.T) { - sm := &SecretManager{} - - auth := &AuthEntry{ - Username: "testuser", - Password: "testpass", +func TestExtractCredentials(t *testing.T) { + tests := []struct { + name string + auth *AuthEntry + expectCreds bool + expectError bool + }{ + { + name: "base64 auth field", + auth: &AuthEntry{ + Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")), + }, + expectCreds: true, + }, + { + name: "separate username/password fields", + auth: &AuthEntry{ + Username: "user", + Password: "pass", + }, + expectCreds: true, + }, + { + name: "nil auth entry", + auth: nil, + expectError: true, + }, + { + name: "empty auth entry", + auth: &AuthEntry{}, + expectError: true, + }, } - creds, err := sm.extractCredentials(auth) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creds, err := extractCredentials(tt.auth) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } + if tt.expectError && err == nil { + t.Error("Expected error, got none") + } - if creds.Username != "testuser" { - t.Errorf("Expected username 'testuser', got: %s", creds.Username) - } + if !tt.expectError && err != nil { + t.Errorf("Expected no error, got: %v", err) + } - if creds.Password != "testpass" { - t.Errorf("Expected password 'testpass', got: %s", creds.Password) + if tt.expectCreds && creds == nil { + t.Error("Expected credentials, got nil") + } + }) } } diff --git a/auto-discovery/kubernetes/pull-secret-extractor/main.go b/auto-discovery/kubernetes/pull-secret-extractor/main.go index 2db42ecb56..00524801a7 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/main.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/main.go @@ -29,16 +29,6 @@ func parseFlags() (*Config, error) { flag.Parse() - // If flags are not provided, use positional arguments - if config.ImageID == "" && config.TemporarySecretName == "" { - args := flag.Args() - if len(args) < 2 { - return nil, fmt.Errorf("usage: program OR use -imageID and -secret flags") - } - config.ImageID = args[0] - config.TemporarySecretName = args[1] - } - if config.ImageID == "" { return nil, fmt.Errorf("image ID is required (use -imageID flag or provide as first argument)") } @@ -56,7 +46,7 @@ func run(config *Config) error { return fmt.Errorf("failed to extract domain from image ID: %s", config.ImageID) } - if err := secret_extraction.CreateTemporarySecret(config.TemporarySecretName, domain); err != nil { + if err := secret_extraction.CreateTemporarySecretFromEnv(config.TemporarySecretName, domain); err != nil { return fmt.Errorf("failed to create temporary secret: %w", err) } diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh index a2c304a094..87854c7118 100755 --- a/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh +++ b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh @@ -35,7 +35,7 @@ spec: initContainers: - name: init-container-test-container image: $1 - args: ["fake-registry.xyz/ubuntu:32131", "test-secret", "default"] + args: ["-imageID", "fake-registry.xyz/ubuntu:32131", "-secret", "test-secret", "default"] volumeMounts: - name: regcred-volume mountPath: "/secrets/regcred" From bf466bc2e9e48db3a8ad56dbe3b970736432446c Mon Sep 17 00:00:00 2001 From: Patrick Weiss Date: Thu, 11 Sep 2025 09:43:33 +0200 Subject: [PATCH 07/10] add licensing information Signed-off-by: Patrick Weiss --- .../internal/secret_extraction/secret_extraction_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go index c4e8cebde0..0028dc393f 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 + package secret_extraction import ( From 5c2079efba43e474699c42136ce858b6adbab33e Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 16 Sep 2025 14:21:12 +0200 Subject: [PATCH 08/10] Replace pod lookup with pod uid env var Signed-off-by: Patrick --- .../secret_extraction/secret_extraction.go | 21 ++++++++++--------- .../secret_extraction_test.go | 2 +- .../test/integration/test-pod.sh | 4 ++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go index 09e08eccbd..f5b56667cf 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go @@ -17,6 +17,7 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" ) @@ -48,7 +49,7 @@ type Credentials struct { Password string } -func CreateTemporarySecret(ctx context.Context, k8sClient client.Client, temporarySecretName, domain, namespace, podName, secretsPath string) error { +func CreateTemporarySecret(ctx context.Context, k8sClient client.Client, temporarySecretName, domain, namespace, podName, podUID, secretsPath string) error { if temporarySecretName == "" { return fmt.Errorf("temporary secret name cannot be empty") } @@ -72,7 +73,7 @@ func CreateTemporarySecret(ctx context.Context, k8sClient client.Client, tempora return fmt.Errorf("failed to extract credentials for domain %s: %w", domain, err) } - secret, err := buildSecret(ctx, k8sClient, temporarySecretName, namespace, podName, creds) + secret, err := buildSecret(ctx, k8sClient, temporarySecretName, namespace, podName, podUID, creds) if err != nil { return fmt.Errorf("failed to build secret: %w", err) } @@ -175,12 +176,7 @@ func extractCredentials(auth *AuthEntry) (*Credentials, error) { return nil, fmt.Errorf("auth entry does not contain valid credentials") } -func buildSecret(ctx context.Context, k8sClient client.Client, secretName, namespace, podName string, creds *Credentials) (*v1.Secret, error) { - pod := &v1.Pod{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: podName, Namespace: namespace}, pod); err != nil { - return nil, fmt.Errorf("failed to get pod %s: %w", podName, err) - } - +func buildSecret(ctx context.Context, k8sClient client.Client, secretName, namespace, podName, podUID string, creds *Credentials) (*v1.Secret, error) { return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, @@ -190,7 +186,7 @@ func buildSecret(ctx context.Context, k8sClient client.Client, secretName, names APIVersion: "v1", Kind: "Pod", Name: podName, - UID: pod.UID, + UID: types.UID(podUID), }, }, }, @@ -232,11 +228,16 @@ func CreateTemporarySecretFromEnv(temporarySecretName, domain string) error { return fmt.Errorf("environment variable %s is not set", envPodName) } + podUID := os.Getenv("POD_UID") + if podUID == "" { + return fmt.Errorf("environment variable %s is not set", podUID) + } + k8sClient, err := createK8sClient() if err != nil { return fmt.Errorf("failed to create Kubernetes client: %w", err) } ctx := context.Background() - return CreateTemporarySecret(ctx, k8sClient, temporarySecretName, domain, namespace, podName, defaultSecretsPath) + return CreateTemporarySecret(ctx, k8sClient, temporarySecretName, domain, namespace, podName, podUID, defaultSecretsPath) } diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go index 0028dc393f..bb0c70f159 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -52,7 +52,7 @@ func TestCreateTemporarySecret(t *testing.T) { os.WriteFile(configPath, configData, 0644) ctx := context.Background() - err := CreateTemporarySecret(ctx, k8sClient, "test-secret", "example.com", "test-namespace", "test-pod", tempDir) + err := CreateTemporarySecret(ctx, k8sClient, "test-secret", "example.com", "test-namespace", "test-pod", "test-uid-123", tempDir) if err != nil { t.Fatalf("Expected no error, got: %v", err) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh index 87854c7118..e118627ec9 100755 --- a/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh +++ b/auto-discovery/kubernetes/pull-secret-extractor/test/integration/test-pod.sh @@ -40,6 +40,10 @@ spec: - name: regcred-volume mountPath: "/secrets/regcred" env: + - name: POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid - name: POD_NAME valueFrom: fieldRef: From bf277ffec2b7c44d038a9573ee2e51d03e7ea28b Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 16 Sep 2025 14:22:16 +0200 Subject: [PATCH 09/10] Add env var in controller Signed-off-by: Patrick --- .../kubernetes/controllers/container_scan_controller.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/auto-discovery/kubernetes/controllers/container_scan_controller.go b/auto-discovery/kubernetes/controllers/container_scan_controller.go index b5915caeab..771d6ad48d 100644 --- a/auto-discovery/kubernetes/controllers/container_scan_controller.go +++ b/auto-discovery/kubernetes/controllers/container_scan_controller.go @@ -354,6 +354,14 @@ func getSecretExtractionInitContainer(imageID string, scanConfig config.ScanConf }, }, }, + { + Name: "POD_UID", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.uid", + }, + }, + }, }, } } From ba035598ee2070c947421a87e51e70f22a792202 Mon Sep 17 00:00:00 2001 From: Jannik Hollenbach Date: Mon, 22 Sep 2025 11:56:33 +0200 Subject: [PATCH 10/10] Fix duplicated base64 encoding of secrets Signed-off-by: Jannik Hollenbach --- .../internal/secret_extraction/secret_extraction.go | 10 +++++----- .../secret_extraction/secret_extraction_test.go | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go index f5b56667cf..83b7e3eaee 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction.go @@ -161,8 +161,8 @@ func extractCredentials(auth *AuthEntry) (*Credentials, error) { } return &Credentials{ - Username: base64.StdEncoding.EncodeToString([]byte(parts[0])), - Password: base64.StdEncoding.EncodeToString([]byte(parts[1])), + Username: parts[0], + Password: parts[1], }, nil } @@ -190,9 +190,9 @@ func buildSecret(ctx context.Context, k8sClient client.Client, secretName, names }, }, }, - Data: map[string][]byte{ - "username": []byte(creds.Username), - "password": []byte(creds.Password), + StringData: map[string]string{ + "username": creds.Username, + "password": creds.Password, }, Type: v1.SecretTypeOpaque, }, nil diff --git a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go index bb0c70f159..99ecb9ffb6 100644 --- a/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go +++ b/auto-discovery/kubernetes/pull-secret-extractor/internal/secret_extraction/secret_extraction_test.go @@ -64,15 +64,15 @@ func TestCreateTemporarySecret(t *testing.T) { t.Fatalf("Secret was not created: %v", err) } - expectedUsername := base64.StdEncoding.EncodeToString([]byte("testuser")) - expectedPassword := base64.StdEncoding.EncodeToString([]byte("testpass")) + expectedUsername := "testuser" + expectedPassword := "testpass" - if string(secret.Data["username"]) != expectedUsername { - t.Errorf("Expected username %s, got %s", expectedUsername, string(secret.Data["username"])) + if secret.StringData["username"] != expectedUsername { + t.Errorf("Expected username %s, got %s", expectedUsername, string(secret.StringData["username"])) } - if string(secret.Data["password"]) != expectedPassword { - t.Errorf("Expected password %s, got %s", expectedPassword, string(secret.Data["password"])) + if secret.StringData["password"] != expectedPassword { + t.Errorf("Expected password %s, got %s", expectedPassword, string(secret.StringData["password"])) } if len(secret.OwnerReferences) != 1 {