Repository: FairwindsOps/polaris
Pull request #821 (2022-08-10 13:26:20)

[FWI-2357] Let Polaris modify YAML without losing comments/formatting
This PR fixes #

## Checklist
* [ ] I have signed the CLA
* [ ] I have updated/added any relevant documentation

## Description
### What's the goal of this PR?

### What changes did you make?

### What alternative solution should we consider, if any?



fairwinds-insights[bot]: ## Fairwinds Insights CI Report
> [View the Full Report](https://insights.fairwinds.com/gh/FairwindsOps/polaris/webhooks)

:white_check_mark: No new Action Items detected!

Patch:
diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml
index 2c3ff3be..ec90fa27 100644
--- a/checks/cpuLimitsMissing.yaml
+++ b/checks/cpuLimitsMissing.yaml
@@ -29,6 +29,4 @@ mutations:
   - op: add
     path: /resources/limits/cpu
     value: 100m
-comments:
-  - find: "cpu: 100m"
     comment: "TODO: Set this to the maximum amount of CPU you want your workload to use"
diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml
index 8d41e428..4a39b50d 100644
--- a/checks/cpuRequestsMissing.yaml
+++ b/checks/cpuRequestsMissing.yaml
@@ -29,6 +29,4 @@ mutations:
   - op: add
     path: /resources/requests/cpu
     value: 100m
-comments:
-  - find: "cpu: 100m"
     comment: "TODO: Set this to the amount of CPU you want to reserve for your workload"
diff --git a/checks/hostPortSet.yaml b/checks/hostPortSet.yaml
index 508bbdbd..91a90ca2 100644
--- a/checks/hostPortSet.yaml
+++ b/checks/hostPortSet.yaml
@@ -12,4 +12,7 @@ schema:
       items:
         properties:
           hostPort:
-            const: 0
\ No newline at end of file
+            const: 0
+mutations:
+  - op: remove
+    path: /ports/*/hostPort
diff --git a/checks/insecureCapabilities.yaml b/checks/insecureCapabilities.yaml
index 1d694840..3526c02e 100644
--- a/checks/insecureCapabilities.yaml
+++ b/checks/insecureCapabilities.yaml
@@ -55,8 +55,6 @@ schema:
                 - contains:
                     pattern: '^(?i)AUDIT_WRITE$'
 mutations:
-  - op: remove
-    path: /securityContext/capabilities
-  - op: add
+  - op: replace
     path: /securityContext/capabilities
     value: {"drop": ["ALL"]}
diff --git a/checks/livenessProbeMissing.yaml b/checks/livenessProbeMissing.yaml
index f62319c3..7db3a78a 100644
--- a/checks/livenessProbeMissing.yaml
+++ b/checks/livenessProbeMissing.yaml
@@ -23,6 +23,4 @@ mutations:
   - op: add
     path: /livenessProbe
     value: {"exec": { "command": [ "cat", "/tmp/healthy" ] }, "initialDelaySeconds": 5, "periodSeconds": 5 }
-comments:
-  - find: "livenessProbe:"
-    comment: "TODO: Change livenessProbe setting to reflect your health endpoints"
\ No newline at end of file
+    comment: "TODO: Change the livenessProbe setting to reflect your application's health"
diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml
index 8cda553e..babba7d2 100644
--- a/checks/memoryLimitsMissing.yaml
+++ b/checks/memoryLimitsMissing.yaml
@@ -29,6 +29,4 @@ mutations:
   - op: add
     path: /resources/limits/memory
     value: "512Mi"
-comments:
-  - find: "memory: 512Mi"
     comment: "TODO: Set this to the maximum amount of memory you want your workload to use"
diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml
index e7ff4d9b..7ea2dcea 100644
--- a/checks/memoryRequestsMissing.yaml
+++ b/checks/memoryRequestsMissing.yaml
@@ -29,6 +29,4 @@ mutations:
   - op: add
     path: /resources/requests/memory
     value: "512Mi"
-comments:
-  - find: "memory: 512Mi"
     comment: "TODO: Set this to the amount of Memory you want to reserve for your workload"
diff --git a/checks/privilegeEscalationAllowed.yaml b/checks/privilegeEscalationAllowed.yaml
index 396f93c8..d21eed86 100644
--- a/checks/privilegeEscalationAllowed.yaml
+++ b/checks/privilegeEscalationAllowed.yaml
@@ -40,4 +40,9 @@ schema:
           - securityContext
           properties:
             securityContext:
-              $ref: "#/definitions/goodSecurityContext"
\ No newline at end of file
+              $ref: "#/definitions/goodSecurityContext"
+
+mutations:
+  - op: add
+    path: /securityContext/allowPrivilegeEscalation
+    value: false
diff --git a/checks/readinessProbeMissing.yaml b/checks/readinessProbeMissing.yaml
index 4401e3b5..94d7ad6a 100644
--- a/checks/readinessProbeMissing.yaml
+++ b/checks/readinessProbeMissing.yaml
@@ -23,6 +23,4 @@ mutations:
   - op: add
     path: /readinessProbe
     value: {"exec": { "command": [ "cat", "/tmp/healthy" ] }, "initialDelaySeconds": 5, "periodSeconds": 5 }
-comments:
-  - find: "readinessProbe:"
-    comment: "TODO: Change livenessProbe setting to reflect your health endpoints"
\ No newline at end of file
+    comment: "TODO: Change the readinessProbe setting to reflect your application's readiness to serve traffic"
diff --git a/cmd/polaris/fix.go b/cmd/polaris/fix.go
index 41a04386..f8c25f7f 100644
--- a/cmd/polaris/fix.go
+++ b/cmd/polaris/fix.go
@@ -15,10 +15,7 @@
 package cmd
 
 import (
-	"bytes"
-	"errors"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -29,19 +26,23 @@ import (
 	"github.com/fairwindsops/polaris/pkg/validator"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
-	yamlV3 "gopkg.in/yaml.v3"
-	"sigs.k8s.io/yaml"
 )
 
+const templateLineMarker = "# POLARIS_FIX_TMPL"
+const templateOpenMarker = "POLARIS_OPEN_TMPL"
+const templateCloseMarker = "POLARIS_CLOSE_TMPL"
+
 var (
 	filesPath   string
 	checksToFix []string
 	fixAll      bool
+	isTemplate  bool
 )
 
 func init() {
 	rootCmd.AddCommand(fixCommand)
 	fixCommand.PersistentFlags().StringVar(&filesPath, "files-path", "", "mutate and fix one or more YAML files in a specified folder")
+	fixCommand.PersistentFlags().BoolVar(&isTemplate, "template", false, "set to true when modifyng a YAML template, like a Helm chart (experimental)")
 	fixCommand.PersistentFlags().StringSliceVar(&checksToFix, "checks", []string{}, "Optional flag to specify specific checks to fix eg. checks=hostIPCSet,hostPIDSet and checks=all applies fix to all defined checks mutations")
 }
 
@@ -53,7 +54,7 @@ var fixCommand = &cobra.Command{
 		logrus.Debug("Setting up controller manager")
 
 		if filesPath == "" {
-			logrus.Error("Please specify a file-path flag")
+			logrus.Error("Please specify a files-path flag")
 			cmd.Help()
 			os.Exit(1)
 		}
@@ -76,8 +77,6 @@ var fixCommand = &cobra.Command{
 		} else {
 			yamlFiles = append(yamlFiles, filesPath)
 		}
-		var contentStr string
-		isFirstResource := true
 
 		if len(checksToFix) > 0 {
 			if len(checksToFix) == 1 && checksToFix[0] == "all" {
@@ -86,75 +85,58 @@ var fixCommand = &cobra.Command{
 					allchecks = append(allchecks, key)
 				}
 				config.Mutations = allchecks
+			} else if len(checksToFix) == 0 && checksToFix[0] == "none" {
+				config.Mutations = nil
 			} else {
 				config.Mutations = checksToFix
 			}
 		}
 
 		for _, fullFilePath := range yamlFiles {
-
-			yamlFile, err := ioutil.ReadFile(fullFilePath)
+			yamlContent, err := ioutil.ReadFile(fullFilePath)
 			if err != nil {
 				logrus.Fatalf("Error reading file with file path %s: %v", fullFilePath, err)
 			}
 
-			dec := yamlV3.NewDecoder(bytes.NewReader(yamlFile))
+			if err != nil {
+				logrus.Fatalf("Error marshalling %s: %v", fullFilePath, err)
+			}
 
-			for {
-				data := map[string]interface{}{}
-				err := dec.Decode(&data)
-				// check it was parsed
-				if data == nil {
-					continue
-				}
-				// break the loop in case of EOF
-				if errors.Is(err, io.EOF) {
-					break
-				}
-				if err != nil {
-					logrus.Fatalf("Error decoding data for file with file path %s: %v", fullFilePath, err)
-				}
-				yamlContent, err := yamlV3.Marshal(data)
-				if err != nil {
-					logrus.Fatalf("Error marshalling %s: %v", fullFilePath, err)
-				}
-				kubeResources := kube.CreateResourceProviderFromYaml(string(yamlContent))
-				results, err := validator.ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
-				if err != nil {
-					logrus.Fatalf("Error applying schema check to the resources %s: %v", fullFilePath, err)
-				}
-				comments, allMutations := mutation.GetMutationsAndCommentsFromResults(results)
-				updatedYamlContent := string(yamlContent)
-				if len(allMutations) > 0 {
-					for _, resources := range kubeResources.Resources {
-						key := fmt.Sprintf("%s/%s/%s", resources[0].Kind, resources[0].Resource.GetName(), resources[0].Resource.GetNamespace())
+			if isTemplate {
+				yamlContent = []byte(detemplate(string(yamlContent)))
+			}
+			kubeResources := kube.CreateResourceProviderFromYaml(string(yamlContent))
+			results, err := validator.ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
+			if err != nil {
+				logrus.Fatalf("Error applying schema check to the resources %s: %v", fullFilePath, err)
+			}
+			allMutations := mutation.GetMutationsFromResults(results)
+
+			updatedYamlContent := ""
+			if len(allMutations) > 0 {
+				for _, resources := range kubeResources.Resources {
+					for _, resource := range resources {
+						key := fmt.Sprintf("%s/%s/%s", resource.Kind, resource.Resource.GetName(), resource.Resource.GetNamespace())
 						mutations := allMutations[key]
-						mutated, err := mutation.ApplyAllSchemaMutations(&config, kubeResources, resources[0], mutations)
+						mutatedYamlContent, err := mutation.ApplyAllMutations(string(resource.OriginalObjectYAML), mutations)
 						if err != nil {
-							logrus.Errorf("Error applying schema mutations to the resources: %v", err)
+							logrus.Errorf("Error applying schema mutations to the resource %s: %v", key, err)
 							os.Exit(1)
 						}
-						mutatedYamlContent, err := yaml.JSONToYAML(mutated.OriginalObjectJSON)
-						if err != nil {
-							logrus.Errorf("Error converting JSON to Yaml : %v", err)
-							os.Exit(1)
+						if updatedYamlContent != "" {
+							updatedYamlContent += "\n---\n"
 						}
-						updatedYamlContent = mutation.UpdateMutatedContentWithComments(string(mutatedYamlContent), comments)
+						updatedYamlContent += mutatedYamlContent
 					}
 				}
-				if isFirstResource {
-					contentStr = updatedYamlContent
-					isFirstResource = false
-				} else {
-					contentStr += "\n"
-					contentStr += "---"
-					contentStr += "\n"
-					contentStr += updatedYamlContent
-				}
 			}
 
-			if contentStr != "" {
-				err = ioutil.WriteFile(fullFilePath, []byte(contentStr), 0644)
+			if isTemplate {
+				updatedYamlContent = retemplate(updatedYamlContent)
+			}
+
+			if updatedYamlContent != "" {
+				err = ioutil.WriteFile(fullFilePath, []byte(updatedYamlContent), 0644)
 				if err != nil {
 					logrus.Fatalf("Error writing output to file: %v", err)
 				}
@@ -164,6 +146,42 @@ var fixCommand = &cobra.Command{
 	},
 }
 
+func detemplate(content string) string {
+	lines := strings.Split(content, "\n")
+	for idx, line := range lines {
+		lines[idx] = detemplateLine(line)
+	}
+	return strings.Join(lines, "\n")
+}
+
+func retemplate(content string) string {
+	lines := strings.Split(content, "\n")
+	for idx, line := range lines {
+		lines[idx] = retemplateLine(line)
+	}
+	return strings.Join(lines, "\n")
+}
+
+func detemplateLine(line string) string {
+	if !strings.HasPrefix(strings.TrimSpace(line), "{{") {
+		line = strings.ReplaceAll(line, "{", templateOpenMarker)
+		line = strings.ReplaceAll(line, "}", templateCloseMarker)
+		return line
+	}
+	tmplStart := strings.Index(line, "{{")
+	newLine := line[:tmplStart] + templateLineMarker + line[tmplStart:]
+	return newLine
+}
+
+func retemplateLine(line string) string {
+	if !strings.Contains(line, templateLineMarker) {
+		line = strings.ReplaceAll(line, templateOpenMarker, "{")
+		line = strings.ReplaceAll(line, templateCloseMarker, "}")
+		return line
+	}
+	return strings.Replace(line, templateLineMarker, "", 1)
+}
+
 func getYamlFiles(rootpath string) ([]string, error) {
 	var list []string
 	err := filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error {
diff --git a/go.mod b/go.mod
index 25bb0742..a891b240 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/fairwindsops/polaris
 go 1.17
 
 require (
-	github.com/evanphx/json-patch/v5 v5.6.0
 	github.com/fatih/color v1.13.0
 	github.com/gobuffalo/packr/v2 v2.8.3
 	github.com/gorilla/mux v1.8.0
@@ -23,7 +22,10 @@ require (
 	sigs.k8s.io/yaml v1.3.0
 )
 
-require gomodules.xyz/jsonpatch/v2 v2.2.0
+require (
+	github.com/pkg/errors v0.9.1
+	gomodules.xyz/jsonpatch/v2 v2.2.0
+)
 
 require (
 	cloud.google.com/go/compute v1.6.1 // indirect
@@ -68,7 +70,6 @@ require (
 	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/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_golang v1.12.2 // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index fe34f3dd..3b622100 100644
--- a/go.sum
+++ b/go.sum
@@ -164,8 +164,6 @@ github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMi
 github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
 github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
-github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -730,8 +728,6 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
-golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
 golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -848,8 +844,6 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/pkg/config/schema.go b/pkg/config/schema.go
index 3a811465..3528acd6 100644
--- a/pkg/config/schema.go
+++ b/pkg/config/schema.go
@@ -25,7 +25,6 @@ import (
 
 	"github.com/qri-io/jsonschema"
 	"github.com/thoas/go-funk"
-	"gomodules.xyz/jsonpatch/v2"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
@@ -53,10 +52,12 @@ var HandledTargets = []TargetKind{
 	TargetPodTemplate,
 }
 
-// MutationComment is the comments added to a mutated file
-type MutationComment struct {
-	Find    string `yaml:"find" json:"find"`
-	Comment string `yaml:"comment" json:"comment"`
+// Mutation defines how to change a YAML file, in the style of JSON Patch
+type Mutation struct {
+	Path    string
+	Op      string
+	Value   interface{}
+	Comment string
 }
 
 // SchemaCheck is a Polaris check that runs using JSON Schema
@@ -75,14 +76,14 @@ type SchemaCheck struct {
 	AdditionalSchemas       map[string]map[string]interface{} `yaml:"additionalSchemas" json:"additionalSchemas"`
 	AdditionalSchemaStrings map[string]string                 `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
 	AdditionalValidators    map[string]jsonschema.RootSchema  `yaml:"-" json:"-"`
-	Mutations               []jsonpatch.Operation             `yaml:"mutations" json:"mutations"`
-	Comments                []MutationComment                 `yaml:"comments" json:"comments"`
+	Mutations               []Mutation                        `yaml:"mutations" json:"mutations"`
 }
 
 type resourceMinimum string
 type resourceMaximum string
 
-func unmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
+// UnmarshalYAMLOrJSON is a helper function to unmarshal data in an arbitrary format
+func UnmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
 	reader := bytes.NewReader(raw)
 	d := k8sYaml.NewYAMLOrJSONDecoder(reader, 4096)
 	for {
@@ -99,7 +100,7 @@ func unmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
 // ParseCheck parses a check from a byte array
 func ParseCheck(id string, rawBytes []byte) (SchemaCheck, error) {
 	check := SchemaCheck{}
-	err := unmarshalYAMLOrJSON(rawBytes, &check)
+	err := UnmarshalYAMLOrJSON(rawBytes, &check)
 	if err != nil {
 		return check, err
 	}
@@ -243,13 +244,13 @@ func (check SchemaCheck) TemplateForResource(res interface{}) (*SchemaCheck, err
 	newCheck.AdditionalValidators = map[string]jsonschema.RootSchema{}
 	for kind, schemaStr := range newCheck.AdditionalSchemaStrings {
 		val := jsonschema.RootSchema{}
-		err := unmarshalYAMLOrJSON([]byte(schemaStr), &val)
+		err := UnmarshalYAMLOrJSON([]byte(schemaStr), &val)
 		if err != nil {
 			return nil, err
 		}
 		newCheck.AdditionalValidators[kind] = val
 	}
-	err := unmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
+	err := UnmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/kube/resource.go b/pkg/kube/resource.go
index 65388ca1..a1d3ca91 100644
--- a/pkg/kube/resource.go
+++ b/pkg/kube/resource.go
@@ -37,6 +37,7 @@ type GenericResource struct {
 	PodSpec            *kubeAPICoreV1.PodSpec
 	PodTemplate        interface{}
 	OriginalObjectJSON []byte
+	OriginalObjectYAML []byte
 }
 
 // NewGenericResourceFromUnstructured creates a workload from an unstructured.Unstructured
@@ -128,7 +129,9 @@ func NewGenericResourceFromBytes(contentBytes []byte) (GenericResource, error) {
 	if err != nil {
 		return GenericResource{}, err
 	}
-	return NewGenericResourceFromUnstructured(unst, nil)
+	res, err := NewGenericResourceFromUnstructured(unst, nil)
+	res.OriginalObjectYAML = contentBytes
+	return res, err
 }
 
 // ResolveControllerFromPod builds a new workload for a given Pod
diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go
index 2b1101ad..534f7221 100644
--- a/pkg/kube/resources.go
+++ b/pkg/kube/resources.go
@@ -471,6 +471,7 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
 		if err != nil {
 			return err
 		}
+		workload.OriginalObjectYAML = contentBytes
 		resources.Resources.addResource(workload)
 	} else {
 		newResource, err := NewGenericResourceFromBytes(contentBytes)
diff --git a/pkg/mutation/mutate.go b/pkg/mutation/mutate.go
index de1a7b8b..7f9ed656 100644
--- a/pkg/mutation/mutate.go
+++ b/pkg/mutation/mutate.go
@@ -1,118 +1,435 @@
 package mutation
 
 import (
-	"bufio"
-	"encoding/json"
+	"bytes"
 	"fmt"
+	"regexp"
+	"strconv"
 	"strings"
 
-	jsonpatchV5 "github.com/evanphx/json-patch/v5"
+	"github.com/sirupsen/logrus"
+	yaml "gopkg.in/yaml.v3"
+
 	"github.com/fairwindsops/polaris/pkg/config"
-	"github.com/fairwindsops/polaris/pkg/kube"
 	"github.com/fairwindsops/polaris/pkg/validator"
-	"github.com/thoas/go-funk"
-	"gomodules.xyz/jsonpatch/v2"
+	"github.com/pkg/errors"
+)
+
+const (
+	strTag  = "!!str"
+	seqTag  = "!!seq"
+	mapTag  = "!!map"
+	intTag  = "!!int"
+	boolTag = "!!bool"
 )
 
-// ApplyAllSchemaMutations applies available mutation to a single resource
-func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource, mutations []jsonpatch.Operation) (kube.GenericResource, error) {
-	resByte := resource.OriginalObjectJSON
-	var jsonByte []byte
-	mutationByte, err := json.Marshal(mutations)
+// ApplyAllMutations applies available mutation to a single resource
+func ApplyAllMutations(manifest string, mutations []config.Mutation) (string, error) {
+	var mutated string
+	var doc yaml.Node
+	err := yaml.Unmarshal([]byte(manifest), &doc)
 	if err != nil {
-		return resource, err
+		return mutated, err
 	}
 
-	patch, err := jsonpatchV5.DecodePatch(mutationByte)
-	if err != nil {
-		return resource, err
+	for _, patch := range mutations {
+		splits := getSplitFromPath(patch.Path)
+		valueNode, err := getNodeFromValue(patch.Value, patch.Comment)
+		if err != nil {
+			return mutated, err
+		}
+		switch patch.Op {
+		case "add", "replace":
+			err = addOrReplaceValue(&doc, splits, valueNode)
+			if err != nil {
+				return mutated, err
+			}
+		case "remove":
+			// ignore error if the value specified does not exists
+			_ = removeNodes(&doc, splits)
+		}
 	}
-	jsonByte, err = patch.ApplyWithOptions(resByte, &jsonpatchV5.ApplyOptions{
-		AllowMissingPathOnRemove: true,
-		EnsurePathExistsOnAdd: true,
-	})
+
+	buf := bytes.Buffer{}
+	enc := yaml.NewEncoder(&buf)
+	enc.SetIndent(2)
+	err = enc.Encode(&doc)
 	if err != nil {
-		return resource, err
+		return mutated, err
 	}
-	mutated, err := kube.NewGenericResourceFromBytes(jsonByte)
+	err = enc.Close()
 	if err != nil {
-		return resource, err
+		return mutated, err
 	}
 
-	return mutated, nil
+	return buf.String(), nil
 }
 
-// GetMutationsAndCommentsFromResults returns all mutations from results
-func GetMutationsAndCommentsFromResults(results []validator.Result) ([]config.MutationComment, map[string][]jsonpatch.Operation) {
-	allMutationsFromResults := make(map[string][]jsonpatch.Operation)
-	comments := []config.MutationComment{}
+// GetMutationsFromResults returns all mutations from results
+func GetMutationsFromResults(results []validator.Result) map[string][]config.Mutation {
+	allMutationsFromResults := make(map[string][]config.Mutation)
 	for _, result := range results {
 		key := fmt.Sprintf("%s/%s/%s", result.Kind, result.Name, result.Namespace)
-
-		mutations, resultsComments := GetMutationsAndCommentsFromResult(&result)
+		mutations := GetMutationsFromResult(&result)
 		allMutationsFromResults[key] = mutations
-		comments = append(comments, resultsComments...)
-
 	}
-	return comments, allMutationsFromResults
+	return allMutationsFromResults
 }
 
-// GetMutationsAndCommentsFromResult returns all mutations from single result
-func GetMutationsAndCommentsFromResult(result *validator.Result) ([]jsonpatch.Operation, []config.MutationComment) {
-	mutations := []jsonpatch.Operation{}
-	comments := []config.MutationComment{}
+// GetMutationsFromResult returns all mutations from single result
+func GetMutationsFromResult(result *validator.Result) []config.Mutation {
+	mutations := []config.Mutation{}
 	for _, resultMessage := range result.Results {
 		if len(resultMessage.Mutations) > 0 {
 			mutations = append(mutations, resultMessage.Mutations...)
 		}
-		if len(resultMessage.Comments) > 0 {
-			comments = append(comments, resultMessage.Comments...)
+	}
+
+	if result.PodResult != nil {
+		for _, resultMessage := range result.PodResult.Results {
+			if len(resultMessage.Mutations) > 0 {
+				mutations = append(mutations, resultMessage.Mutations...)
+			}
+		}
+
+		for _, containerResult := range result.PodResult.ContainerResults {
+			for _, resultMessage := range containerResult.Results {
+				if len(resultMessage.Mutations) > 0 {
+					mutations = append(mutations, resultMessage.Mutations...)
+				}
+			}
 		}
 	}
 
-	for _, resultMessage := range result.PodResult.Results {
-		if len(resultMessage.Mutations) > 0 {
-			mutations = append(mutations, resultMessage.Mutations...)
+	return mutations
+}
+
+func createPathAndFindNodes(node *yaml.Node, selectors []string, create bool) ([]*yaml.Node, error) {
+	var nodes []*yaml.Node
+	currentSelector := selectors[0]
+	isLastSelector := len(selectors) == 1
+	// array[N] or array[*] selectors.
+	if i := strings.LastIndex(currentSelector, "["); i > 0 && strings.HasSuffix(currentSelector, "]") {
+		arrayIndex := currentSelector[i+1 : len(currentSelector)-1]
+		currentSelector = currentSelector[:i]
+		if checkIfNodeExistedInContent(node.Content, currentSelector) || !create {
+			return findArrayNodes(selectors, currentSelector, node, nodes, arrayIndex, create)
+		}
+		index, err := strconv.Atoi(arrayIndex)
+		if err != nil {
+			if arrayIndex != "-" {
+				return nil, errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
+			}
+			// if index provided is greater than or less than 0 for an empty array should throw an exception
+		} else if index != 0 {
+			return nil, errors.Errorf("array index (%s) does not exists because array (%s) does not exists", arrayIndex, currentSelector)
+		}
+		// default to zero since no node is present.
+		selectorsToCreateNodes := []string{currentSelector, "0"}
+		if len(selectors) > 1 {
+			selectorsToCreateNodes = append(selectorsToCreateNodes, selectors[1:]...)
+			return createNonExistingPath(selectorsToCreateNodes, node), nil
+		}
+	}
+	if currentSelector == "-" {
+		if isLastSelector {
+			return []*yaml.Node{node}, nil
 		}
-		if len(resultMessage.Comments) > 0 {
-			comments = append(comments, resultMessage.Comments...)
+		_, err := createPathAndFindNodes(node, selectors[1:], create)
+		if err != nil {
+			return nil, err
 		}
 	}
+	switch node.Kind {
+	case yaml.MappingNode:
+		for i := 0; i < len(node.Content); i += 2 {
+			// Does the current key match the selector?
+			if node.Content[i].Value == currentSelector {
+				// Found last key, return its value.
+				if !isLastSelector {
+					// Match the rest of the selector path, ie. go deeper
+					// in to the value node.
+					return createPathAndFindNodes(node.Content[i+1], selectors[1:], create)
+				}
+				return []*yaml.Node{node.Content[i+1]}, nil
+			}
+		}
+	case yaml.ScalarNode:
+		// Overwrite any existing nodes.
+		node.Kind = yaml.MappingNode
+		node.Tag = mapTag
+		node.Value = ""
+	case yaml.SequenceNode:
+		return nil, errors.Errorf("parent node is array, use /*/ or /0/../%v/ instead of .%v to access its item(s) first", len(node.Content)-1, currentSelector)
 
-	for _, containerResult := range result.PodResult.ContainerResults {
-		for _, resultMessage := range containerResult.Results {
-			if len(resultMessage.Mutations) > 0 {
-				mutations = append(mutations, resultMessage.Mutations...)
+	default:
+		return nil, errors.Errorf("parent node is of unknown kind %v", node.Kind)
+	}
+	if !create {
+		return nil, errors.Errorf("failed to match %s", strings.Join(selectors, "/"))
+	}
+
+	return createNonExistingPath(selectors, node), nil
+}
+
+func addOrReplaceValue(node *yaml.Node, splits []string, value *yaml.Node) error {
+	if len(node.Content) == 0 {
+		return errors.New("No content in node")
+	}
+	nodes, err := createPathAndFindNodes(node.Content[0], splits, true)
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes {
+		if node.Kind == yaml.ScalarNode {
+			// Overwrite an existing scalar value with a new value (whatever kind).
+			*node = *value
+		} else if node.Kind == yaml.MappingNode && value.Kind == yaml.MappingNode {
+			// Append new values onto an existing map node.
+			if len(value.Content) < 2 {
+				logrus.Error("Found a mapping node without enough content")
+				continue
+			}
+			mapKey := value.Content[0].Value
+			found := false
+			for idx, subNode := range node.Content {
+				if idx % 2 != 0 {
+					continue
+				}
+				if subNode.Value == mapKey {
+					found = true
+					node.Content[idx+1] = value.Content[1]
+				}
+			}
+			if !found {
+				node.Content = append(value.Content, node.Content...)
+			}
+		} else if node.Kind == yaml.MappingNode && node.Content == nil {
+			// Overwrite a new map node we created in createPathAndFindNodes(), as confirmed
+			// by the nil check (the node.Content wouldn't be nil otherwise).
+			*node = *value
+		} else if node.Kind == yaml.SequenceNode && value.Kind == yaml.SequenceNode {
+			// Append new values onto an existing array node.
+			node.Content = append(node.Content, value.Content...)
+		} else if node.Kind == yaml.SequenceNode && value.Kind == yaml.ScalarNode {
+			// Append new value onto an existing array node.
+			node.Content = append(node.Content, value)
+		} else {
+			return errors.Errorf("can't overwrite %v value (line: %v, column: %v) with %v value", node.Tag, node.Line, node.Column, value.Tag)
+		}
+	}
+
+	return nil
+}
+
+func getNodeFromValue(value interface{}, comment string) (*yaml.Node, error) {
+	bytes, err := yaml.Marshal(value)
+	if err != nil {
+		return nil, err
+	}
+	var doc yaml.Node
+	err = yaml.Unmarshal(bytes, &doc)
+	if err != nil {
+		return nil, err
+	}
+	if len(doc.Content) == 0 {
+		return nil, errors.New("Generated an empty YAML document")
+	}
+	if doc.Content[0].Kind == yaml.MappingNode {
+		doc.Content[0].Content[0].HeadComment = comment
+	} else {
+		doc.Content[0].LineComment = comment
+	}
+	return doc.Content[0], nil
+}
+
+func removeNodes(doc *yaml.Node, selectors []string) error {
+	err := removeMatchingNode(doc.Content[0], selectors)
+	if err != nil {
+		return errors.Wrapf(err, "failed to match %q", strings.Join(selectors, "/"))
+	}
+
+	return nil
+}
+
+func removeMatchingNode(node *yaml.Node, selectors []string) error {
+	currentSelector := selectors[0]
+	lastSelector := len(selectors) == 1
+	if i := strings.LastIndex(currentSelector, "["); i > 0 && strings.HasSuffix(currentSelector, "]") {
+		arrayIndex := currentSelector[i+1 : len(currentSelector)-1]
+		currentSelector = currentSelector[:i]
+
+		index, err := strconv.Atoi(arrayIndex)
+		if err != nil {
+			if arrayIndex == "*" {
+				index = -1
+			} else {
+				return errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
+			}
+		} else if index < 0 {
+			return errors.Wrapf(err, "array index can't be negative %v/%v/", currentSelector, arrayIndex)
+		}
+		// Go into array node(s).
+		arrayNodes, err := createPathAndFindNodes(node, []string{currentSelector}, false)
+		if err != nil {
+			return errors.Errorf("can't find %v", currentSelector)
+		}
+		for _, arrayNode := range arrayNodes {
+			if arrayNode.Kind != yaml.SequenceNode {
+				return errors.Errorf("%v is not an array", currentSelector)
+			}
+			if index >= len(arrayNode.Content) {
+				return errors.Errorf("%v array doesn't have index %v", currentSelector, index)
 			}
-			if len(resultMessage.Comments) > 0 {
-				comments = append(comments, resultMessage.Comments...)
+
+			var visitArrayNodes []*yaml.Node
+			if index >= 0 { // array[N]
+				visitArrayNodes = []*yaml.Node{arrayNode.Content[index]}
+			} else { // array[*]
+				visitArrayNodes = arrayNode.Content
+			}
+			for _, node := range visitArrayNodes {
+				lastSelector := len(selectors) == 1
+				if !lastSelector {
+					removeMatchingNode(node, selectors[1:])
+				}
+			}
+		}
+	}
+
+	// Iterate over the keys (the slice is key/value pairs).
+	for i := 0; i < len(node.Content); i += 2 {
+		if node.Content[i].Value == currentSelector {
+			// Key matches the selector.
+			if !lastSelector {
+				// Try to match the rest of the selector path in the value.
+				return removeMatchingNode(node.Content[i+1], selectors[1:])
+			}
+
+			node.Content[i] = nil   // Delete key.
+			node.Content[i+1] = nil // Delete value.
+			node.Content = append(node.Content[:i], node.Content[i+2:]...)
+			return nil
+		}
+	}
+
+	return errors.Errorf("can't find %q", strings.Join(selectors, "."))
+}
+
+func getSplitFromPath(path string) []string {
+	var digitStarCheck = regexp.MustCompile(`^[0-9*]+$`)
+	splits := strings.Split(path, "/")
+	var formatedSplit []string
+	for _, key := range splits {
+		if key == "" {
+			continue
+		}
+		if digitStarCheck.MatchString(key) {
+			lastElementIdx := len(formatedSplit) - 1
+			lastElement := formatedSplit[lastElementIdx]
+			lastElement = fmt.Sprintf("%s[%s]", lastElement, key)
+			formatedSplit[lastElementIdx] = lastElement
+			continue
+		}
+		formatedSplit = append(formatedSplit, key)
+	}
+	return formatedSplit
+}
+
+func findArrayNodes(selectors []string, currentSelector string, node *yaml.Node, nodes []*yaml.Node, arrayIndex string, create bool) ([]*yaml.Node, error) {
+
+	index, err := strconv.Atoi(arrayIndex)
+	if err != nil {
+		if arrayIndex == "*" {
+			index = -1
+		} else {
+			return nil, errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
+		}
+	} else if index < 0 {
+		return nil, errors.Wrapf(err, "array index can't be negative %v/%v/", currentSelector, arrayIndex)
+	}
+	// Go into array node(s).
+	arrayNodes, err := createPathAndFindNodes(node, []string{currentSelector}, create)
+	if err != nil {
+		return nil, errors.Errorf("can't find %v", currentSelector)
+	}
+	for _, arrayNode := range arrayNodes {
+		if arrayNode.Kind != yaml.SequenceNode {
+			return nil, errors.Errorf("%v is not an array", currentSelector)
+		}
+		if index >= len(arrayNode.Content) {
+			return nil, errors.Errorf("%v array doesn't have index %v", currentSelector, index)
+		}
+
+		var visitArrayNodes []*yaml.Node
+		if index >= 0 { // array[N]
+			visitArrayNodes = []*yaml.Node{arrayNode.Content[index]}
+		} else { // array[*]
+			visitArrayNodes = arrayNode.Content
+		}
+
+		for i, node := range visitArrayNodes {
+			if len(selectors) == 1 {
+				// Last selector, use this as final node.
+				nodes = append(nodes, node)
+			} else {
+				// Go deeper into a specific array.
+				deeperNodes, err := createPathAndFindNodes(node, selectors[1:], create)
+				if err != nil {
+					return nil, errors.Wrapf(err, "failed to go deeper into %v/%v/", currentSelector, i)
+				}
+				nodes = append(nodes, deeperNodes...)
 			}
 		}
 	}
+	return nodes, nil
+}
+
+func checkIfNodeExistedInContent(nodes []*yaml.Node, currentSelector string) bool {
+	for i := 0; i < len(nodes); i += 2 {
+		// Does the current key match the selector?
+		if nodes[i].Value == currentSelector {
+			return true
+		}
+	}
 
-	return mutations, comments
+	return false
 }
 
-// UpdateMutatedContentWithComments Updates mutated object with comments
-func UpdateMutatedContentWithComments(yamlContent string, comments []config.MutationComment) string {
-	var lines []string
-	scanner := bufio.NewScanner(strings.NewReader(yamlContent))
-	scanner.Split(bufio.ScanLines)
-	for scanner.Scan() {
-		line := scanner.Text()
-		lines = append(lines, line)
-	}
-	commentMap := funk.Map(comments, func(c config.MutationComment) (string, string) {
-		return c.Find, c.Comment
-	}).(map[string]string)
-	fileContent := ""
-	for _, line := range lines {
-		comment, ok := commentMap[strings.TrimSpace(line)]
-		if ok {
-			line += (" #" + comment)
-		}
-		fileContent += line
-		fileContent += "\n"
-	}
-	return fileContent
+func createNonExistingPath(selectors []string, node *yaml.Node) []*yaml.Node {
+	var digitDashCheck = regexp.MustCompile(`^[0-9-]+$`)
+	// Create the rest of the selector path.
+	for idx, selector := range selectors {
+		if digitDashCheck.MatchString(selector) {
+			continue
+		}
+		kind, tag := yaml.MappingNode, mapTag
+		// if the next selector is "-" then current selector is sequence/slice/array
+		if idx < len(selectors)-1 && digitDashCheck.MatchString(selectors[idx+1]) {
+			kind, tag = yaml.SequenceNode, seqTag
+		}
+		var newNode = yaml.Node{
+			Content: []*yaml.Node{
+				{
+					Kind:  yaml.ScalarNode,
+					Tag:   strTag,
+					Value: selector,
+				},
+				{
+					Kind: kind,
+					Tag:  tag,
+				},
+			},
+		}
+		// if previous node is array/sequenceNode append a node rather than appending contents
+		if node.Kind == yaml.SequenceNode {
+			newNode.Kind = kind
+			node.Content = append(node.Content, &newNode)
+		} else {
+			node.Content = append(node.Content, newNode.Content...)
+		}
+		node = newNode.Content[len(newNode.Content)-1]
+	}
+	return []*yaml.Node{node}
+
 }
diff --git a/pkg/validator/output.go b/pkg/validator/output.go
index 3e328a6e..1f996e04 100644
--- a/pkg/validator/output.go
+++ b/pkg/validator/output.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/fatih/color"
 	"github.com/thoas/go-funk"
-	"gomodules.xyz/jsonpatch/v2"
 
 	"github.com/fairwindsops/polaris/pkg/config"
 )
@@ -89,8 +88,7 @@ type ResultMessage struct {
 	Success   bool
 	Severity  config.Severity
 	Category  string
-	Mutations []jsonpatch.Operation
-	Comments  []config.MutationComment
+	Mutations []config.Mutation
 }
 
 // ResultSet contiains the results for a set of checks
diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go
index df2040a3..b6b01536 100644
--- a/pkg/validator/schema.go
+++ b/pkg/validator/schema.go
@@ -24,7 +24,6 @@ import (
 	"github.com/qri-io/jsonschema"
 	"github.com/sirupsen/logrus"
 	"github.com/thoas/go-funk"
-	"gomodules.xyz/jsonpatch/v2"
 	corev1 "k8s.io/api/core/v1"
 	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -381,13 +380,12 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
 	result := makeResult(conf, check, passes, issues)
 	if !passes {
 		if funk.Contains(conf.Mutations, checkID) && len(check.Mutations) > 0 {
-			mutations := funk.Map(check.Mutations, func(mutation jsonpatch.Operation) jsonpatch.Operation {
+			mutations := funk.Map(check.Mutations, func(mutation config.Mutation) config.Mutation {
 				mutationCopy := deepCopyMutation(mutation)
 				mutationCopy.Path = prefix + mutationCopy.Path
 				return mutationCopy
-			}).([]jsonpatch.Operation)
+			}).([]config.Mutation)
 			result.Mutations = mutations
-			result.Comments = check.Comments
 		}
 	}
 	return &result, nil
@@ -402,11 +400,12 @@ func getSortedKeys(m map[string]config.Severity) []string {
 	return keys
 }
 
-func deepCopyMutation(source jsonpatch.Operation) jsonpatch.Operation {
-	destination := jsonpatch.Operation{
-		Operation: source.Operation,
-		Path:      source.Path,
-		Value:     source.Value,
+func deepCopyMutation(source config.Mutation) config.Mutation {
+	destination := config.Mutation{
+		Op:      source.Op,
+		Path:    source.Path,
+		Value:   source.Value,
+		Comment: source.Comment,
 	}
 	return destination
 }
diff --git a/pkg/webhook/mutate.go b/pkg/webhook/mutate.go
index 3a3461b6..40e50409 100644
--- a/pkg/webhook/mutate.go
+++ b/pkg/webhook/mutate.go
@@ -25,6 +25,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+	"sigs.k8s.io/yaml"
 )
 
 // Mutator mutate k8s resources.
@@ -44,12 +45,20 @@ func NewMutateWebhook(mgr manager.Manager, mutator Mutator) {
 }
 
 func (m *Mutator) mutate(req admission.Request) ([]jsonpatch.Operation, error) {
-	results, err := GetValidatedResults(req.AdmissionRequest.Kind.Kind, m.decoder, req, m.Config)
+	results, kubeResources, err := GetValidatedResults(req.AdmissionRequest.Kind.Kind, m.decoder, req, m.Config)
 	if err != nil {
 		return nil, err
 	}
-	patches, _ := mutation.GetMutationsAndCommentsFromResult(results)
-	return patches, nil
+	patches := mutation.GetMutationsFromResult(results)
+	originalYaml, err := yaml.JSONToYAML(kubeResources.OriginalObjectJSON)
+	if err != nil {
+		return nil, err
+	}
+	mutatedYamlStr, err := mutation.ApplyAllMutations(string(originalYaml), patches)
+	if err != nil {
+		return nil, err
+	}
+	return jsonpatch.CreatePatch(originalYaml, []byte(mutatedYamlStr))
 }
 
 // Handle for Validator to run validation checks.
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index f71d02e3..66e3f246 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -54,43 +54,42 @@ func NewValidateWebhook(mgr manager.Manager, validator Validator) {
 	mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator})
 }
 
-func (v *Validator) handleInternal(req admission.Request) (*validator.Result, error) {
+func (v *Validator) handleInternal(req admission.Request) (*validator.Result, kube.GenericResource, error) {
 	return GetValidatedResults(req.AdmissionRequest.Kind.Kind, v.decoder, req, v.Config)
 }
 
 // GetValidatedResults returns the validated results.
-func GetValidatedResults(kind string, decoder *admission.Decoder, req admission.Request, config config.Configuration) (*validator.Result, error) {
+func GetValidatedResults(kind string, decoder *admission.Decoder, req admission.Request, config config.Configuration) (*validator.Result, kube.GenericResource, error) {
 	var controller kube.GenericResource
 	var err error
 	if kind == "Pod" {
 		pod := corev1.Pod{}
 		err := decoder.Decode(req, &pod)
 		if err != nil {
-			return nil, err
+			return nil, controller, err
 		}
 		if len(pod.ObjectMeta.OwnerReferences) > 0 {
 			logrus.Infof("Allowing owned pod %s/%s to pass through webhook", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
-			return nil, nil
+			return nil, controller, nil
 		}
 		controller, err = kube.NewGenericResourceFromPod(pod, pod)
 	} else {
 		controller, err = kube.NewGenericResourceFromBytes(req.Object.Raw)
 	}
 	if err != nil {
-		return nil, err
+		return nil, controller, err
 	}
-	// TODO: consider enabling multi-resource checks
 	controllerResult, err := validator.ApplyAllSchemaChecks(&config, nil, controller)
 	if err != nil {
-		return nil, err
+		return nil, controller, err
 	}
-	return &controllerResult, nil
+	return &controllerResult, controller, nil
 }
 
 // Handle for Validator to run validation checks.
 func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
 	logrus.Info("Starting request")
-	result, err := v.handleInternal(req)
+	result, _, err := v.handleInternal(req)
 	if err != nil {
 		logrus.Errorf("Error validating request: %v", err)
 		return admission.Errored(http.StatusBadRequest, err)


Test patch:
diff --git a/pkg/mutation/mutate_test.go b/pkg/mutation/mutate_test.go
new file mode 100644
index 00000000..97cb8e89
--- /dev/null
+++ b/pkg/mutation/mutate_test.go
@@ -0,0 +1,157 @@
+package mutation
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/fairwindsops/polaris/pkg/config"
+)
+
+var oldYaml = `
+pets:
+  - name: fido
+    owners:
+      - name: Alice
+      - name: Bob
+        aliases:
+        - Robert
+  - name: scooby
+`
+
+var testCases = []struct{
+	original string
+	mutated string
+	patch config.Mutation
+	message string
+}{{
+	original: oldYaml,
+	patch: config.Mutation{
+		Op:    "add",
+		Value: "Denver",
+		Path:  "/pets/0/owners/*/location",
+	},
+	mutated: `pets:
+  - name: fido
+    owners:
+      - name: Alice
+        location: Denver
+      - name: Bob
+        aliases:
+          - Robert
+        location: Denver
+  - name: scooby
+`,
+}, {
+	original: oldYaml,
+	patch: config.Mutation{
+		Op:   "remove",
+		Path: "/pets/0/owners/*/aliases",
+	},
+	mutated: `pets:
+  - name: fido
+    owners:
+      - name: Alice
+      - name: Bob
+  - name: scooby
+`,
+}, {
+	original: oldYaml,
+	patch: config.Mutation{
+		Op:    "add",
+		Value: "rob",
+		Path:  "/pets/0/owners/*/aliases/-",
+	},
+	mutated: `pets:
+  - name: fido
+    owners:
+      - name: Alice
+        aliases:
+          - rob
+      - name: Bob
+        aliases:
+          - Robert
+          - rob
+  - name: scooby
+`,
+}, {
+	original: `
+pets:
+  - name: fido
+`,
+	patch: config.Mutation{
+		Op:    "add",
+		Value: "Alice",
+		Path:  "/pets/0/owners/0/name",
+	},
+	mutated: `pets:
+  - name: fido
+    owners:
+      - name: Alice
+`,
+}, {
+	original: `
+obj:
+  foo:
+    bar:
+      - a
+      - b
+  baz: quux
+`,
+	patch: config.Mutation{
+		Op: "replace",
+		Value: map[string]interface{}{
+			"bar": []string{"c", "d"},
+		},
+		Path: "/obj/foo",
+	},
+	mutated: `obj:
+  foo:
+    bar:
+      - c
+      - d
+  baz: quux
+`,
+}, {
+	original:`
+foo: bar
+`,
+	patch: config.Mutation{
+		Op: "replace",
+		Value: "baz",
+		Path: "/foo",
+		Comment: "# We set this to baz",
+	},
+	mutated: `
+foo: baz # We set this to baz
+`,
+	message: "Expected a comment to appear",
+}, {
+	original:`
+foo: bar
+`,
+	patch: config.Mutation{
+		Op: "add",
+		Value: map[string]interface{}{
+			"baz": "quux",
+		},
+		Path: "/extra",
+		Comment: "# These are extra things",
+	},
+	mutated: `
+foo: bar
+extra:
+  # These are extra things
+  baz: quux
+`,
+	message: "Expected a comment to appear next to an object",
+}}
+
+func TestApplyAllMutations(t *testing.T) {
+	for _, tc := range testCases {
+		mutated, err := ApplyAllMutations(tc.original, []config.Mutation{tc.patch})
+		assert.NoError(t, err)
+		assert.EqualValues(t, strings.TrimSpace(tc.mutated), strings.TrimSpace(mutated), tc.message)
+	}
+}
diff --git a/test/checks/cpuLimitsMissing/mutated.yaml b/test/checks/cpuLimitsMissing/mutated.yaml
index 9e693d88..6bcd7e3f 100644
--- a/test/checks/cpuLimitsMissing/mutated.yaml
+++ b/test/checks/cpuLimitsMissing/mutated.yaml
@@ -1,14 +1,14 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources:
-      limits:
-        cpu: 100m #TODO: Set this to the maximum amount of CPU you want your workload to use
-        memory: 128Mi
+    - name: nginx
+      image: nginx
+      resources:
+        limits:
+          memory: 128Mi
+          cpu: 100m # TODO: Set this to the maximum amount of CPU you want your workload to use
diff --git a/test/checks/cpuRequestsMissing/mutated.yaml b/test/checks/cpuRequestsMissing/mutated.yaml
index 386cfc65..7153bc39 100644
--- a/test/checks/cpuRequestsMissing/mutated.yaml
+++ b/test/checks/cpuRequestsMissing/mutated.yaml
@@ -1,14 +1,14 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources:
-      requests:
-        cpu: 100m #TODO: Set this to the amount of CPU you want to reserve for your workload
-        memory: 128Mi
+    - name: nginx
+      image: nginx
+      resources:
+        requests:
+          memory: 128Mi
+          cpu: 100m # TODO: Set this to the amount of CPU you want to reserve for your workload
diff --git a/test/checks/dangerousCapabilities/mutated.yaml b/test/checks/dangerousCapabilities/mutated.yaml
index 5c978f18..a8fadc16 100644
--- a/test/checks/dangerousCapabilities/mutated.yaml
+++ b/test/checks/dangerousCapabilities/mutated.yaml
@@ -1,12 +1,11 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
-    securityContext: {}
+    - name: nginx
+      image: nginx
+      securityContext: {}
diff --git a/test/checks/deploymentMissingReplicas/mutated.yaml b/test/checks/deploymentMissingReplicas/mutated.yaml
index d11eca20..54eef6c9 100644
--- a/test/checks/deploymentMissingReplicas/mutated.yaml
+++ b/test/checks/deploymentMissingReplicas/mutated.yaml
@@ -1,9 +1,9 @@
 apiVersion: v1
 kind: Deployment
 metadata:
+  name: nginx
   labels:
     env: test
-  name: nginx
 spec:
   replicas: 3
   template:
diff --git a/test/checks/hostIPCSet/mutated.yaml b/test/checks/hostIPCSet/mutated.yaml
index 1b0b62e6..f5309b0d 100644
--- a/test/checks/hostIPCSet/mutated.yaml
+++ b/test/checks/hostIPCSet/mutated.yaml
@@ -1,11 +1,10 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
+    - name: nginx
+      image: nginx
diff --git a/test/checks/hostNetworkSet/mutated.yaml b/test/checks/hostNetworkSet/mutated.yaml
index 1b0b62e6..f5309b0d 100644
--- a/test/checks/hostNetworkSet/mutated.yaml
+++ b/test/checks/hostNetworkSet/mutated.yaml
@@ -1,11 +1,10 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
+    - name: nginx
+      image: nginx
diff --git a/test/checks/hostPIDSet/mutated.yaml b/test/checks/hostPIDSet/mutated.yaml
index 1b0b62e6..f5309b0d 100644
--- a/test/checks/hostPIDSet/mutated.yaml
+++ b/test/checks/hostPIDSet/mutated.yaml
@@ -1,11 +1,10 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
+    - name: nginx
+      image: nginx
diff --git a/test/checks/hostPortSet/mutated.yaml b/test/checks/hostPortSet/mutated.yaml
new file mode 100644
index 00000000..662ef56b
--- /dev/null
+++ b/test/checks/hostPortSet/mutated.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: nginx
+  labels:
+    app.kubernetes.io/name: nginx
+spec:
+  containers:
+    - name: nginx
+      image: nginx
+      ports:
+        - containerPort: 80
diff --git a/test/checks/insecureCapabilities/mutated.drop-most.yaml b/test/checks/insecureCapabilities/mutated.drop-most.yaml
index 2222f047..b782362e 100644
--- a/test/checks/insecureCapabilities/mutated.drop-most.yaml
+++ b/test/checks/insecureCapabilities/mutated.drop-most.yaml
@@ -1,15 +1,14 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     env: test
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
-    securityContext:
-      capabilities:
-        drop:
-        - ALL
+    - name: nginx
+      image: nginx
+      securityContext:
+        capabilities:
+          drop:
+            - ALL
diff --git a/test/checks/insecureCapabilities/mutated.yaml b/test/checks/insecureCapabilities/mutated.yaml
index 2222f047..b782362e 100644
--- a/test/checks/insecureCapabilities/mutated.yaml
+++ b/test/checks/insecureCapabilities/mutated.yaml
@@ -1,15 +1,14 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     env: test
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources: {}
-    securityContext:
-      capabilities:
-        drop:
-        - ALL
+    - name: nginx
+      image: nginx
+      securityContext:
+        capabilities:
+          drop:
+            - ALL
diff --git a/test/checks/livenessProbeMissing/mutated.yaml b/test/checks/livenessProbeMissing/mutated.yaml
index 0c8b43f0..68878382 100644
--- a/test/checks/livenessProbeMissing/mutated.yaml
+++ b/test/checks/livenessProbeMissing/mutated.yaml
@@ -6,17 +6,17 @@ metadata:
   name: liveness-exec
 spec:
   containers:
-  - args:
-    - /bin/sh
-    - -c
-    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
-    image: k8s.gcr.io/busybox
-    livenessProbe: #TODO: Change livenessProbe setting to reflect your health endpoints
-      exec:
-        command:
-        - cat
-        - /tmp/healthy
-      initialDelaySeconds: 5
-      periodSeconds: 5
-    name: liveness
-    resources: {}
+    - name: liveness
+      image: k8s.gcr.io/busybox
+      args:
+        - /bin/sh
+        - -c
+        - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
+      livenessProbe:
+        # TODO: Change livenessProbe setting to reflect your health endpoints
+        exec:
+          command:
+            - cat
+            - /tmp/healthy
+        initialDelaySeconds: 5
+        periodSeconds: 5
diff --git a/test/checks/memoryLimitsMissing/mutated.cpu.yaml b/test/checks/memoryLimitsMissing/mutated.cpu.yaml
index 2a4659f0..98f8fbce 100644
--- a/test/checks/memoryLimitsMissing/mutated.cpu.yaml
+++ b/test/checks/memoryLimitsMissing/mutated.cpu.yaml
@@ -1,14 +1,14 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources:
-      limits:
-        cpu: 100m
-        memory: 512Mi #TODO: Set this to the maximum amount of memory you want your workload to use
+    - name: nginx
+      image: nginx
+      resources:
+        limits:
+          cpu: 100m
+          memory: 512Mi # TODO: Set this to the maximum amount of memory you want your workload to use
diff --git a/test/checks/memoryLimitsMissing/mutated.yaml b/test/checks/memoryLimitsMissing/mutated.yaml
index 67200ce6..e4e30f3b 100644
--- a/test/checks/memoryLimitsMissing/mutated.yaml
+++ b/test/checks/memoryLimitsMissing/mutated.yaml
@@ -1,13 +1,13 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources:
-      limits:
-        memory: 512Mi #TODO: Set this to the maximum amount of memory you want your workload to use
+    - name: nginx
+      image: nginx
+      resources:
+        limits:
+          memory: 512Mi # TODO: Set this to the maximum amount of memory you want your workload to use
diff --git a/test/checks/memoryRequestsMissing/mutated.yaml b/test/checks/memoryRequestsMissing/mutated.yaml
index 432dc1ab..86167aad 100644
--- a/test/checks/memoryRequestsMissing/mutated.yaml
+++ b/test/checks/memoryRequestsMissing/mutated.yaml
@@ -1,13 +1,13 @@
 apiVersion: v1
 kind: Pod
 metadata:
+  name: nginx
   labels:
     app.kubernetes.io/name: nginx
-  name: nginx
 spec:
   containers:
-  - image: nginx
-    name: nginx
-    resources:
-      requests:
-        memory: 512Mi #TODO: Set this to the amount of Memory you want to reserve for your workload
+    - name: nginx
+      image: nginx
+      resources:
+        requests:
+          memory: 512Mi # TODO: Set this to the amount of Memory you want to reserve for your workload
diff --git a/test/checks/pullPolicyNotAlways/mutated.cron-job.yaml b/test/checks/pullPolicyNotAlways/mutated.cron-job.yaml
index 67443f9b..51c1229f 100644
--- a/test/checks/pullPolicyNotAlways/mutated.cron-job.yaml
+++ b/test/checks/pullPolicyNotAlways/mutated.cron-job.yaml
@@ -3,17 +3,17 @@ kind: CronJob
 metadata:
   name: hello
 spec:
+  schedule: "* * * * *"
   jobTemplate:
     spec:
       template:
         spec:
           containers:
-          - command:
-            - /bin/sh
-            - -c
-            - date; echo Hello from the Kubernetes cluster
-            image: busybox
-            imagePullPolicy: Always
-            name: hello
+            - name: hello
+              image: busybox
+              command:
+                - /bin/sh
+                - -c
+                - date; echo Hello from the Kubernetes cluster
+              imagePullPolicy: Always
           restartPolicy: OnFailure
-  schedule: '* * * * *'
diff --git a/test/checks/pullPolicyNotAlways/mutated.deployment.yaml b/test/checks/pullPolicyNotAlways/mutated.deployment.yaml
index 2855ec1d..daa76211 100644
--- a/test/checks/pullPolicyNotAlways/mutated.deployment.yaml
+++ b/test/checks/pullPolicyNotAlways/mutated.deployment.yaml
@@ -15,8 +15,8 @@ spec:
         app: nginx
     spec:
       containers:
-      - image: nginx:1.14.2
-        imagePullPolicy: Always
-        name: nginx
-        ports:
-        - containerPort: 80
+        - image: nginx:1.14.2
+          imagePullPolicy: Always
+          name: nginx
+          ports:
+            - containerPort: 80
diff --git a/test/checks/pullPolicyNotAlways/mutated.yaml b/test/checks/pullPolicyNotAlways/mutated.yaml
index 80d48d19..2ef164fd 100644
--- a/test/checks/pullPolicyNotAlways/mutated.yaml
+++ b/test/checks/pullPolicyNotAlways/mutated.yaml
@@ -6,7 +6,6 @@ metadata:
   name: nginx
 spec:
   containers:
-  - image: nginx
-    imagePullPolicy: Always
-    name: nginx
-    resources: {}
+    - image: nginx
+      imagePullPolicy: Always
+      name: nginx
diff --git a/test/checks/readinessProbeMissing/mutated.yaml b/test/checks/readinessProbeMissing/mutated.yaml
index 97d3cbec..80eee975 100644
--- a/test/checks/readinessProbeMissing/mutated.yaml
+++ b/test/checks/readinessProbeMissing/mutated.yaml
@@ -6,17 +6,17 @@ metadata:
   name: readiness-exec
 spec:
   containers:
-  - args:
-    - /bin/sh
-    - -c
-    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
-    image: k8s.gcr.io/busybox
-    name: readiness
-    readinessProbe: #TODO: Change livenessProbe setting to reflect your health endpoints
-      exec:
-        command:
-        - cat
-        - /tmp/healthy
-      initialDelaySeconds: 5
-      periodSeconds: 5
-    resources: {}
+    - name: readiness
+      image: k8s.gcr.io/busybox
+      args:
+        - /bin/sh
+        - -c
+        - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
+      readinessProbe:
+        # TODO: Change readinessProbe setting to reflect your application's readiness to serve traffic
+        exec:
+          command:
+            - cat
+            - /tmp/healthy
+        initialDelaySeconds: 5
+        periodSeconds: 5
diff --git a/test/checks/runAsRootAllowed/mutated.yaml b/test/checks/runAsRootAllowed/mutated.yaml
index d660e41b..a22fc5ad 100644
--- a/test/checks/runAsRootAllowed/mutated.yaml
+++ b/test/checks/runAsRootAllowed/mutated.yaml
@@ -6,7 +6,7 @@ spec:
   template:
     spec:
       containers:
-      - image: nginx
-        name: nginx
-        securityContext:
-          runAsNonRoot: true
+        - name: nginx
+          image: nginx
+          securityContext:
+            runAsNonRoot: true
diff --git a/test/mutation_test.go b/test/mutation_test.go
index 92bee9eb..bd4aaa0c 100644
--- a/test/mutation_test.go
+++ b/test/mutation_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/fairwindsops/polaris/pkg/mutation"
 	"github.com/fairwindsops/polaris/pkg/validator"
 	"github.com/stretchr/testify/assert"
-	"sigs.k8s.io/yaml"
 )
 
 var configYaml = `
@@ -18,6 +17,7 @@ checks:
   hostIPCSet: danger
   hostPIDSet: danger
   hostNetworkSet: danger
+  hostPortSet: warning
   deploymentMissingReplicas: warning
   priorityClassNotSet: ignore
   runAsRootAllowed: danger
@@ -49,18 +49,15 @@ func TestMutations(t *testing.T) {
 			results, err := validator.ApplyAllSchemaChecksToResourceProvider(&newConfig, tc.resources)
 			assert.NoError(t, err)
 			assert.Len(t, results, 1)
-			comments, allMutations := mutation.GetMutationsAndCommentsFromResults(results)
+			allMutations := mutation.GetMutationsFromResults(results)
 			assert.Len(t, allMutations, 1)
 			for _, resources := range tc.resources.Resources {
 				assert.Len(t, resources, 1)
 				key := fmt.Sprintf("%s/%s/%s", resources[0].Kind, resources[0].Resource.GetName(), resources[0].Resource.GetNamespace())
 				mutations := allMutations[key]
-				mutated, err := mutation.ApplyAllSchemaMutations(&c, tc.resources, resources[0], mutations)
+				yamlContent, err := mutation.ApplyAllMutations(tc.manifest, mutations)
 				assert.NoError(t, err)
-				yamlContent, err := yaml.JSONToYAML(mutated.OriginalObjectJSON)
-				assert.NoError(t, err)
-				contentStr := mutation.UpdateMutatedContentWithComments(string(yamlContent), comments)
-				assert.EqualValues(t, mutatedYamlContent, contentStr, "Mutation test case for " + tc.check + "/" + tc.filename + " failed")
+				assert.EqualValues(t, mutatedYamlContent, yamlContent, "Mutation test case for "+tc.check+"/"+tc.filename+" failed")
 			}
 		}
 	}
diff --git a/test/schema_test.go b/test/schema_test.go
index 3c0a1372..1dcb32e1 100644
--- a/test/schema_test.go
+++ b/test/schema_test.go
@@ -38,6 +38,7 @@ type testCase struct {
 	resources *kube.ResourceProvider
 	failure   bool
 	config    config.Configuration
+	manifest  string
 }
 
 var mutatedYamlContentMap = map[string]string{}
@@ -80,6 +81,10 @@ func init() {
 			if tc.Name() == "check.yaml" {
 				continue
 			}
+			yamlContent, err := os.ReadFile(checkDir + "/" + tc.Name())
+			if err != nil {
+				panic(err)
+			}
 			resourceFilename := strings.Replace(tc.Name(), "mutated", "failure", -1)
 
 			resources, err := kube.CreateResourceProviderFromPath(checkDir + "/" + resourceFilename)
@@ -92,13 +97,10 @@ func init() {
 				resources: resources,
 				failure:   strings.Contains(resourceFilename, "failure"),
 				config:    c,
+				manifest:  string(yamlContent),
 			}
 
 			if strings.Contains(tc.Name(), "mutated") {
-				yamlContent, err := os.ReadFile(checkDir + "/" + tc.Name())
-				if err != nil {
-					panic(err)
-				}
 				key := fmt.Sprintf("%s/%s", check, tc.Name())
 				mutatedYamlContentMap[key] = string(yamlContent)
 				testCases, ok := mutationTestCasesMap[check]