Repository: sse-secure-systems/connaisseur (go)

Issue:
Issue with ArgoCD Connaisseur change tag in deployment resource to digest
**Describe the bug**
Connaisseur version 3.5.0 and 3.4.0 have issue with mutation of deployment objects which cause argocd to stay out-of-sync.

I want to disable mutation of some kubernetes objects like deployments,statefulset and deamonset.
Tried following:
1) Set mode in policy definition (with.mode) to insecureValidateOnly. Nothing happend argocd still modified deployment objects.
2) Tried with resource validation mode (https://sse-secure-systems.github.io/connaisseur/v3.5.0/features/resource_validation_mode/) set resourceValidationMode to "podsOnly", got following error in argocd

one or more objects failed to apply, reason: Internal error occurred: failed calling webhook "connaisseur-svc.connaisseur.svc": received invalid webhook response: webhook returned response.patchType but not response.patch

**Expected behavior**
Dont want connaisseur to mutate deployment objects just pods.

**Optional: Versions (please complete the following information as relevant):**
 - Kubernetes Cluster: 1.28.9
 - Container registry: jfrog
 - Connaisseur: 3.5.0


Pull request:
fix: return empty patch type if there is no patch
When Connaisseur was done with the verification and returned an empty patch, the patch type was still set to JSONPatch. Kubernetes handled this regardless, but other tools like ArgoCD did not and threw an error. Connaisseur will now neither set a patch nor patch type and leave those fields empty, so it's compatible with other tools.

Fixes #1705 

## Checklist
<!--- Mark as done if a point is not necessary. Feel free to reach out if help on any items in the checklist is needed. -->

- [x] PR is rebased to/aimed at branch `develop`
- [x] PR follows [Contributing Guide](https://github.com/sse-secure-systems/connaisseur/blob/master/docs/CONTRIBUTING.md)
- [x] Added tests (if necessary)
- [x] Extended README/Documentation (if necessary)
- [x] Adjusted versions of image and Helm chart in `Chart.yaml` (if necessary)


Patch:
diff --git a/internal/handler/mutate.go b/internal/handler/mutate.go
index 9a58d15e..03ed5bb9 100644
--- a/internal/handler/mutate.go
+++ b/internal/handler/mutate.go
@@ -104,6 +104,7 @@ func handleMutate(ctx context.Context, w http.ResponseWriter, r *http.Request) {
 				}
 				response.Response.Allowed = true
 				response.Response.Patch = nil
+				response.Response.PatchType = nil
 			}
 
 			// detection mode
@@ -250,21 +251,23 @@ func mutateReview(ctx context.Context, ar admission.AdmissionReview) (*admission
 		}
 		skipped = skipped && valOut.Skipped
 	}
-	pt := admission.PatchTypeJSONPatch
-	jsonPatchesStr := "[" + strings.Join(jsonPatches, ",") + "]"
-	imagesStr := strings.Join(images, ", ")
-	notificationValues.Images = imagesStr
 
+	ares := admission.AdmissionResponse{
+		UID:     ar.Request.UID,
+		Allowed: true,
+	}
+	if len(jsonPatches) > 0 {
+		pt := admission.PatchTypeJSONPatch
+		ares.PatchType = &pt
+		ares.Patch = []byte("[" + strings.Join(jsonPatches, ",") + "]")
+	}
+
+	notificationValues.Images = strings.Join(images, ", ")
 	if skipped {
 		notificationValues.Result = constants.NotificationResultSkip
 	} else {
 		notificationValues.Result = constants.NotificationResultSuccess
 	}
 
-	return &admission.AdmissionResponse{
-		UID:       ar.Request.UID,
-		Allowed:   true,
-		PatchType: &pt,
-		Patch:     []byte(jsonPatchesStr),
-	}, notificationValues
+	return &ares, notificationValues
 }


Test patch:
diff --git a/internal/handler/mutate_test.go b/internal/handler/mutate_test.go
index 5f81e3df..a384aa24 100644
--- a/internal/handler/mutate_test.go
+++ b/internal/handler/mutate_test.go
@@ -369,7 +369,7 @@ func TestMutateReview(t *testing.T) {
 		{ // 3: Admits with static_allow and doesn't cache, producing no patch
 			"18_allow_me_pod",
 			true,
-			"[]",
+			"",
 			"my.reg/allow-me@sha256:91ac9b26df583762234c1cdb2fc930364754ccc59bc752a2bfe298d2ea68f9ff",
 			"",
 			"",
@@ -387,7 +387,7 @@ func TestMutateReview(t *testing.T) {
 		{ // 5: Admits with static_allow and caches success, producing no patch, even though index.docker.io/library would be appended
 			"22_allow_me_with_index_docker_io",
 			true,
-			"[]",
+			"",
 			"test-image",
 			"",
 			"",
@@ -396,7 +396,7 @@ func TestMutateReview(t *testing.T) {
 		{ // 6: Admits and stores correct hash, when validation is successful. Produces no patch, since image reference doesn't change (sha only)
 			"23_alice_digest_dpl",
 			true,
-			`[]`,
+			"",
 			"securesystemsengineering/alice-image@sha256:1234567890123456123456789012345612345678901234561234567890123456",
 			"{\"digest\":\"sha256:1234567890123456123456789012345612345678901234561234567890123456\",\"error\":\"\"}",
 			"",
@@ -405,7 +405,7 @@ func TestMutateReview(t *testing.T) {
 		{ // 7: Admits and stores correct hash, when validation is successful. Produces no patch, since image reference doesn't change (tag+sha)
 			"24_alice_tag_digest_dpl",
 			true,
-			`[]`,
+			"",
 			"securesystemsengineering/alice-image:test@sha256:1234567890123456123456789012345612345678901234561234567890123456",
 			"{\"digest\":\"sha256:1234567890123456123456789012345612345678901234561234567890123456\",\"error\":\"\"}",
 			"",
@@ -441,8 +441,10 @@ func TestMutateReview(t *testing.T) {
 		// Patch as expected or not present if not expected
 		if tc.patch != "" {
 			assert.Equalf(t, []byte(tc.patch), res.Patch, "test case %d", i+1)
+			assert.Equalf(t, v1.PatchTypeJSONPatch, *res.PatchType, "test case %d", i+1)
 		} else {
 			assert.Nil(t, res.Patch, "test case %d", i+1)
+			assert.Nil(t, res.PatchType, "test case %d", i+1)
 		}
 
 		// Cache value is as expected
@@ -527,7 +529,7 @@ func TestMutateReviewCached(t *testing.T) {
 		{ // Admits without patch
 			"18_allow_me_pod",
 			true,
-			"[]",
+			"",
 			"my.reg/allow-me@sha256:91ac9b26df583762234c1cdb2fc930364754ccc59bc752a2bfe298d2ea68f9ff",
 			"{\"digest\":\"sha256:91ac9b26df583762234c1cdb2fc930364754ccc59bc752a2bfe298d2ea68f9ff\",\"error\":\"\"}",
 			"",