Repository: tenable/terrascan (language: python)

Issue:
Add support to extract rules to skip from terraform comments
This PR adds support for extracting rule ids to skip policy evaluation. These rules can be set as a comma separated list of rule ids in the comments of terraform resource config in the following pattern:
```hcl
#ts:skip=AWS.S3Bucket.DS.High.1041, AWS.S3Bucket.DS.High.1042
```

Example of a terraform resource config with rules to skip policy evaluation:
```hcl
resource "aws_s3_bucket" "bucket2" {
   #ts:skip=AWS.S3Bucket.DS.High.1041
   region        = var.region
   bucket        = local.bucket_name
   #ts:skip=AWS.S3Bucket.DS.High.1044,AWS.S3Bucket.DS.High.1045
   force_destroy = true
   #ts:skip= AWS.S3Bucket.DS.High.1046   ,   AWS.S3Bucket.DS.High.1047
   acl           = "public-read"
 }
```


Discussion / hints:

Fix patch:
diff --git a/pkg/cli/init.go b/pkg/cli/init.go
index e05a23fce..db97e1db9 100644
--- a/pkg/cli/init.go
+++ b/pkg/cli/init.go
@@ -29,15 +29,18 @@ var initCmd = &cobra.Command{
 
 Initializes Terrascan and clones policies from the Terrascan GitHub repository.
 `,
-	Run: initial,
+	RunE:          initial,
+	SilenceUsage:  true,
+	SilenceErrors: true,
 }
 
-func initial(cmd *cobra.Command, args []string) {
+func initial(cmd *cobra.Command, args []string) error {
 	// initialize terrascan
 	if err := initialize.Run(); err != nil {
 		zap.S().Error("failed to initialize terrascan")
-		return
+		return err
 	}
+	return nil
 }
 
 func init() {
diff --git a/pkg/cli/register.go b/pkg/cli/register.go
index 81fd7fc3a..98da7e4b9 100644
--- a/pkg/cli/register.go
+++ b/pkg/cli/register.go
@@ -18,7 +18,6 @@ package cli
 
 import (
 	"flag"
-	"fmt"
 	"os"
 
 	"github.com/accurics/terrascan/pkg/config"
@@ -31,27 +30,6 @@ func RegisterCommand(baseCommand *cobra.Command, command *cobra.Command) {
 	baseCommand.AddCommand(command)
 }
 
-func subCommands() (commandNames []string) {
-	for _, command := range rootCmd.Commands() {
-		commandNames = append(commandNames, append(command.Aliases, command.Name())...)
-	}
-	return
-}
-
-// setDefaultCommand sets `scan` as default command if no other command is specified
-func setDefaultCommandIfNonePresent() {
-	if len(os.Args) > 1 {
-		potentialCommand := os.Args[1]
-		for _, command := range subCommands() {
-			if command == potentialCommand {
-				return
-			}
-		}
-		os.Args = append([]string{os.Args[0], "scan"}, os.Args[1:]...)
-	}
-
-}
-
 // Execute the entrypoint called by main
 func Execute() {
 	rootCmd.PersistentFlags().StringVarP(&LogLevel, "log-level", "l", "info", "log level (debug, info, warn, error, panic, fatal)")
@@ -81,9 +59,7 @@ func Execute() {
 		}
 	}
 
-	setDefaultCommandIfNonePresent()
 	if err := rootCmd.Execute(); err != nil {
-		fmt.Println(err)
 		os.Exit(1)
 	}
 }
diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go
index 23aa3c482..14461cc18 100644
--- a/pkg/cli/scan.go
+++ b/pkg/cli/scan.go
@@ -35,15 +35,17 @@ var scanCmd = &cobra.Command{
 
 Detect compliance and security violations across Infrastructure as Code to mitigate risk before provisioning cloud native infrastructure.
 `,
-	PreRun: initial,
-	Run:    scan,
+	PreRunE:       initial,
+	RunE:          scan,
+	SilenceUsage:  true,
+	SilenceErrors: true,
 }
 
-func scan(cmd *cobra.Command, args []string) {
+func scan(cmd *cobra.Command, args []string) error {
 	zap.S().Debug("running terrascan in cli mode")
 	scanOptions.configFile = ConfigFile
 	scanOptions.outputType = OutputType
-	scanOptions.Scan()
+	return scanOptions.Scan()
 }
 
 func init() {
diff --git a/pkg/iac-providers/kubernetes/v1/normalize.go b/pkg/iac-providers/kubernetes/v1/normalize.go
index 39825cdab..f6346cd97 100644
--- a/pkg/iac-providers/kubernetes/v1/normalize.go
+++ b/pkg/iac-providers/kubernetes/v1/normalize.go
@@ -24,19 +24,23 @@ import (
 	"github.com/accurics/terrascan/pkg/utils"
 	yamltojson "github.com/ghodss/yaml"
 	"github.com/iancoleman/strcase"
+	"go.uber.org/zap"
 	"gopkg.in/yaml.v3"
 )
 
+const terrascanSkip = "terrascanSkip"
+
 var (
 	errUnsupportedDoc = fmt.Errorf("unsupported document type")
 	// ErrNoKind is returned when the "kind" key is not available (not a valid kubernetes resource)
 	ErrNoKind = fmt.Errorf("kind does not exist")
 )
 
-// k8sMetadata is used to pull the name and namespace types for a given resource
+// k8sMetadata is used to pull the name, namespace types and annotations for a given resource
 type k8sMetadata struct {
-	Name      string `yaml:"name" json:"name"`
-	Namespace string `yaml:"namespace" json:"namespace"`
+	Name        string                 `yaml:"name" json:"name"`
+	Namespace   string                 `yaml:"namespace" json:"namespace"`
+	Annotations map[string]interface{} `yaml:"annotations" json:"annotations"`
 }
 
 // k8sResource is a generic struct to handle all k8s resource types
@@ -116,6 +120,12 @@ func (k *K8sV1) Normalize(doc *utils.IacDocument) (*output.ResourceConfig, error
 		resourceConfig.ID = resourceConfig.Type + "." + resource.Metadata.Name + "." + namespace
 	}
 
+	// read and update skip rules, if present
+	skipRules := readSkipRulesFromAnnotations(resource.Metadata.Annotations, resourceConfig.ID)
+	if skipRules != nil {
+		resourceConfig.SkipRules = append(resourceConfig.SkipRules, skipRules...)
+	}
+
 	configData := make(map[string]interface{})
 	if err = json.Unmarshal(*jsonData, &configData); err != nil {
 		return nil, err
@@ -126,3 +136,28 @@ func (k *K8sV1) Normalize(doc *utils.IacDocument) (*output.ResourceConfig, error
 
 	return &resourceConfig, nil
 }
+
+func readSkipRulesFromAnnotations(annotations map[string]interface{}, resourceID string) []string {
+
+	var skipRulesFromAnnotations interface{}
+	var ok bool
+	if skipRulesFromAnnotations, ok = annotations[terrascanSkip]; !ok {
+		zap.S().Debugf("%s not present for resource: %s", terrascanSkip, resourceID)
+		return nil
+	}
+
+	skipRules := make([]string, 0)
+	if rules, ok := skipRulesFromAnnotations.([]interface{}); ok {
+		for _, rule := range rules {
+			if value, ok := rule.(string); ok {
+				skipRules = append(skipRules, value)
+			} else {
+				zap.S().Debugf("each rule in %s must be of string type", terrascanSkip)
+			}
+		}
+	} else {
+		zap.S().Debugf("%s must be an array of rules to skip", terrascanSkip)
+	}
+
+	return skipRules
+}
diff --git a/pkg/iac-providers/output/types.go b/pkg/iac-providers/output/types.go
index 2b52a2403..cfa19c2db 100644
--- a/pkg/iac-providers/output/types.go
+++ b/pkg/iac-providers/output/types.go
@@ -24,6 +24,10 @@ type ResourceConfig struct {
 	Line   int         `json:"line"`
 	Type   string      `json:"type"`
 	Config interface{} `json:"config"`
+	// SkipRules will hold the rules to be skipped for the resource.
+	// Each iac provider should append the rules to be skipped for a resource,
+	// while extracting resource from the iac files
+	SkipRules []string `json:"skip_rules"`
 }
 
 // AllResourceConfigs is a list/slice of resource configs present in IaC
diff --git a/pkg/iac-providers/terraform/v12/resource.go b/pkg/iac-providers/terraform/v12/resource.go
index 1d3f96d77..1fbba9074 100644
--- a/pkg/iac-providers/terraform/v12/resource.go
+++ b/pkg/iac-providers/terraform/v12/resource.go
@@ -20,11 +20,11 @@ import (
 	"fmt"
 	"io/ioutil"
 
+	"github.com/accurics/terrascan/pkg/iac-providers/output"
+	"github.com/accurics/terrascan/pkg/utils"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
 	hclConfigs "github.com/hashicorp/terraform/configs"
 	"go.uber.org/zap"
-
-	"github.com/accurics/terrascan/pkg/iac-providers/output"
 )
 
 // CreateResourceConfig creates output.ResourceConfig
@@ -49,12 +49,13 @@ func CreateResourceConfig(managedResource *hclConfigs.Resource) (resourceConfig
 
 	// create a resource config
 	resourceConfig = output.ResourceConfig{
-		ID:     fmt.Sprintf("%s.%s", managedResource.Type, managedResource.Name),
-		Name:   managedResource.Name,
-		Type:   managedResource.Type,
-		Source: managedResource.DeclRange.Filename,
-		Line:   managedResource.DeclRange.Start.Line,
-		Config: goOut,
+		ID:        fmt.Sprintf("%s.%s", managedResource.Type, managedResource.Name),
+		Name:      managedResource.Name,
+		Type:      managedResource.Type,
+		Source:    managedResource.DeclRange.Filename,
+		Line:      managedResource.DeclRange.Start.Line,
+		Config:    goOut,
+		SkipRules: utils.GetSkipRules(c.rangeSource(hclBody.Range())),
 	}
 
 	// successful
diff --git a/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json b/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json
new file mode 100644
index 000000000..fa346d61e
--- /dev/null
+++ b/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json
@@ -0,0 +1,14 @@
+{
+    "name": "disAllowedVolumes",
+    "file": "disAllowedVolumes.rego",
+    "template_args": {
+        "name": "disAllowedVolumes",
+        "prefix": "",
+        "suffix": ""
+    },
+    "severity": "MEDIUM",
+    "description": "Vulnerable to CVE-2020-8555",
+    "reference_id": "AC-K8-DS-PO-M-0143",
+    "category": "Network Security",
+    "version": 1
+}
\ No newline at end of file
diff --git a/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego b/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego
new file mode 100644
index 000000000..706bbafa8
--- /dev/null
+++ b/pkg/policies/opa/rego/k8s/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego
@@ -0,0 +1,52 @@
+package accurics
+
+#rule for pod_security_policy
+{{.prefix}}{{.name}}{{.suffix}}[psp.id] {
+    psp := input.kubernetes_pod_security_policy[_]
+    affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"]
+    volume_type := psp.config.spec.volumes[_]
+    volNotAllowed(volume_type, affected_volumes)
+}
+
+#rule for pod
+{{.prefix}}{{.name}}{{.suffix}}[pod.id] {
+    pod := input.kubernetes_pod[_]
+    affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"]
+    volume_types := {x | pod.config.spec.volumes[_][x]; x != "name"}
+    vol:= volume_types[_]
+    volNotAllowed(vol, affected_volumes)
+}
+
+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller
+{{.prefix}}{{.name}}{{.suffix}}[kind.id] {
+    item_list := [
+        object.get(input, "kubernetes_daemonset", "undefined"),
+        object.get(input, "kubernetes_deployment", "undefined"),
+        object.get(input, "kubernetes_job", "undefined"),
+        object.get(input, "kubernetes_replica_set", "undefined"),
+        object.get(input, "kubernetes_replication_controller", "undefined"),
+        object.get(input, "kubernetes_stateful_set", "undefined")
+    ]
+
+    item = item_list[_]
+    item != "undefined"
+
+    kind := item[_]
+    affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"]
+    volume_types := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"}
+    vol:= volume_types[_]
+    volNotAllowed(vol, affected_volumes)
+}
+
+#rule for cron_job
+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] {
+    cron_job := input.kubernetes_cron_job[_]
+    affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"]
+    volume_types := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"}
+    vol:= volume_types[_]
+    volNotAllowed(vol, affected_volumes)
+}
+
+volNotAllowed(field, affected_volumes) {
+    field == affected_volumes[_]
+}
\ No newline at end of file
diff --git a/pkg/policy/opa/engine.go b/pkg/policy/opa/engine.go
index 8126faffe..09777e0b0 100644
--- a/pkg/policy/opa/engine.go
+++ b/pkg/policy/opa/engine.go
@@ -377,6 +377,22 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput,
 				zap.S().Error(err)
 				continue
 			}
+
+			// do no report violations if rule is skipped for resource
+			if len(resource.SkipRules) > 0 {
+				found := false
+				for _, rule := range resource.SkipRules {
+					if strings.EqualFold(k, rule) {
+						found = true
+						break
+					}
+				}
+				if found {
+					zap.S().Debugf("rule: %s skipped for resource: %s", k, resource.Name)
+					continue
+				}
+			}
+
 			if resource == nil {
 				zap.S().Warn("resource was not found", zap.String("resource id", resourceID))
 				continue
diff --git a/pkg/utils/skip_rules.go b/pkg/utils/skip_rules.go
new file mode 100644
index 000000000..08e464d6b
--- /dev/null
+++ b/pkg/utils/skip_rules.go
@@ -0,0 +1,51 @@
+/*
+    Copyright (C) 2020 Accurics, Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+*/
+
+package utils
+
+import (
+	"regexp"
+	"strings"
+)
+
+var (
+	skipRulesPattern = regexp.MustCompile(`#ts:skip=\s*(([A-Za-z0-9]+\.?){5})(\s*,\s*([A-Za-z0-9]+\.?){5})*`)
+	skipRulesPrefix  = "#ts:skip="
+)
+
+// GetSkipRules returns a list of rules to be skipped. The rules to be skipped
+// can be set in terraform resource config with the following comma separated pattern:
+// #ts:skip=AWS.S3Bucket.DS.High.1043, AWS.S3Bucket.DS.High.1044
+func GetSkipRules(body string) []string {
+
+	var skipRules []string
+
+	// check if any rules comments are present in body
+	if !skipRulesPattern.MatchString(body) {
+		return skipRules
+	}
+
+	// get all skip rule comments
+	comments := skipRulesPattern.FindAllString(body, -1)
+
+	// extract rule ids from comments
+	for _, c := range comments {
+		c = strings.TrimPrefix(c, skipRulesPrefix)
+		c = strings.ReplaceAll(c, ",", " ")
+		skipRules = append(skipRules, strings.Fields(c)...)
+	}
+	return skipRules
+}


Test patch:
diff --git a/pkg/cli/testdata/run-test/test_pod.yaml b/pkg/cli/testdata/run-test/test_pod.yaml
index dada1636b..925a9fd34 100644
--- a/pkg/cli/testdata/run-test/test_pod.yaml
+++ b/pkg/cli/testdata/run-test/test_pod.yaml
@@ -6,6 +6,8 @@ metadata:
     app: myapp
     test: someupdate
     test2: someupdate3
+  annotations:
+    terrascanSkip: [accurics.kubernetes.IAM.109]
 spec:
   containers:
   - name: myapp-container
@@ -22,6 +24,8 @@ metadata:
     app: myapp
     test: someupdate
     test2: someupdate3
+  annotations:
+    terrascanSkip: [accurics.kubernetes.IAM.3, accurics.kubernetes.OPS.461]
 spec:
   template:
     spec:
diff --git a/pkg/iac-providers/kubernetes/v1/load-file_test.go b/pkg/iac-providers/kubernetes/v1/load-file_test.go
index 8adf81474..cc507c23a 100644
--- a/pkg/iac-providers/kubernetes/v1/load-file_test.go
+++ b/pkg/iac-providers/kubernetes/v1/load-file_test.go
@@ -66,6 +66,12 @@ func TestLoadIacFile(t *testing.T) {
 			k8sV1:    K8sV1{},
 			wantErr:  nil,
 		},
+		{
+			name:     "file with skip rules in annotations",
+			filePath: "./testdata/file-test-data/test_pod_skip_rules.yaml",
+			k8sV1:    K8sV1{},
+			wantErr:  nil,
+		},
 	}
 
 	for _, tt := range table {
diff --git a/pkg/iac-providers/kubernetes/v1/normalize_test.go b/pkg/iac-providers/kubernetes/v1/normalize_test.go
new file mode 100644
index 000000000..5b9f05634
--- /dev/null
+++ b/pkg/iac-providers/kubernetes/v1/normalize_test.go
@@ -0,0 +1,312 @@
+/*
+    Copyright (C) 2020 Accurics, Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+*/
+
+package k8sv1
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/accurics/terrascan/pkg/iac-providers/output"
+	"github.com/accurics/terrascan/pkg/utils"
+)
+
+var (
+	testJSONData = []byte(`{
+		"kind": "Pod",
+		"apiVersion": "v1",
+		"metadata": {
+		  "name": "simple"
+		},
+		"spec": {
+		  "containers": [
+			{
+			  "name": "healthz",
+			  "image": "k8s.gcr.io/exechealthz-amd64:1.2",
+			  "args": [
+				"-cmd=nslookup localhost"
+			  ],
+			  "ports": [
+				{
+				  "containerPort": 8080,
+				  "protocol": "TCP"
+				}
+			  ]
+			}
+		  ]
+		}
+	  }`)
+
+	testYAMLData = []byte(`apiVersion: v1
+kind: Pod
+metadata:
+  name: myapp-pod
+  annotations:
+    terrascanSkip: [accurics.kubernetes.IAM.109]
+spec:
+  containers:
+    - name: myapp-container
+      image: busybox`)
+)
+
+func TestK8sV1ExtractResource(t *testing.T) {
+	type args struct {
+		doc *utils.IacDocument
+	}
+	tests := []struct {
+		name    string
+		k       *K8sV1
+		args    args
+		want    *k8sResource
+		wantErr bool
+	}{
+		{
+			name: "empty document object",
+			args: args{
+				doc: &utils.IacDocument{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "json document object",
+			args: args{
+				doc: &utils.IacDocument{
+					Type: "json",
+					Data: testJSONData,
+				},
+			},
+			want: &k8sResource{
+				APIVersion: "v1",
+				Kind:       "Pod",
+				Metadata: k8sMetadata{
+					Name: "simple",
+				},
+			},
+		},
+		{
+			name: "yaml document object",
+			args: args{
+				doc: &utils.IacDocument{
+					Type: "yaml",
+					Data: testYAMLData,
+				},
+			},
+			want: &k8sResource{
+				APIVersion: "v1",
+				Kind:       "Pod",
+				Metadata: k8sMetadata{
+					Name: "myapp-pod",
+					Annotations: map[string]interface{}{
+						terrascanSkip: []interface{}{"accurics.kubernetes.IAM.109"},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			k := &K8sV1{}
+			got, _, err := k.extractResource(tt.args.doc)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("K8sV1.extractResource() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("K8sV1.extractResource() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestK8sV1GetNormalizedName(t *testing.T) {
+	type args struct {
+		kind string
+	}
+	tests := []struct {
+		name string
+		k    *K8sV1
+		args args
+		want string
+	}{
+		{
+			name: "normalized name for pod",
+			args: args{
+				kind: "pod",
+			},
+			want: "kubernetes_pod",
+		},
+		{
+			name: "normalized name for DaemonSet",
+			args: args{
+				kind: "DaemonSet",
+			},
+			want: "kubernetes_daemonset",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			k := &K8sV1{}
+			if got := k.getNormalizedName(tt.args.kind); got != tt.want {
+				t.Errorf("K8sV1.getNormalizedName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestK8sV1Normalize(t *testing.T) {
+	testRule := "accurics.kubernetes.IAM.109"
+
+	type args struct {
+		doc *utils.IacDocument
+	}
+	tests := []struct {
+		name    string
+		k       *K8sV1
+		args    args
+		want    *output.ResourceConfig
+		wantErr bool
+	}{
+		{
+			name: "empty iac document object",
+			args: args{
+				&utils.IacDocument{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "valid iac document object",
+			args: args{
+				&utils.IacDocument{
+					Type: "yaml",
+					Data: testYAMLData,
+				},
+			},
+			want: &output.ResourceConfig{
+				ID:   "kubernetes_pod.myapp-pod.default",
+				Name: "myapp-pod",
+				Line: 0,
+				Type: "kubernetes_pod",
+				Config: map[string]interface{}{
+					"apiVersion": "v1",
+					"kind":       "Pod",
+					"metadata": map[string]interface{}{
+						"annotations": map[string]interface{}{
+							terrascanSkip: []interface{}{testRule},
+						},
+						"name": "myapp-pod",
+					},
+					"spec": map[string]interface{}{
+						"containers": []interface{}{
+							map[string]interface{}{
+								"image": "busybox",
+								"name":  "myapp-container",
+							},
+						},
+					},
+				},
+				SkipRules: []string{testRule},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			k := &K8sV1{}
+			got, err := k.Normalize(tt.args.doc)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("K8sV1.Normalize() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("K8sV1.Normalize() got = %+v, want = %+v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestReadSkipRulesFromAnnotations(t *testing.T) {
+	// test data
+	testRuleA := "RuleA"
+	testRuleB := "RuleB"
+	testRuleC := "RuleC"
+
+	type args struct {
+		annotations map[string]interface{}
+		resourceID  string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "nil annotations",
+			args: args{
+				annotations: nil,
+			},
+		},
+		{
+			name: "annotations with no terrascanSkipRules",
+			args: args{
+				annotations: map[string]interface{}{
+					"test": "test",
+				},
+			},
+		},
+		{
+			name: "annotations with invalid terrascanSkipRules type",
+			args: args{
+				annotations: map[string]interface{}{
+					terrascanSkip: "test",
+				},
+			},
+			want: []string{},
+		},
+		{
+			name: "annotations with invalid terrascanSkipRules rule value",
+			args: args{
+				annotations: map[string]interface{}{
+					terrascanSkip: []interface{}{1},
+				},
+			},
+			want: []string{},
+		},
+		{
+			name: "annotations with one terrascanSkipRules",
+			args: args{
+				annotations: map[string]interface{}{
+					terrascanSkip: []interface{}{testRuleA},
+				},
+			},
+			want: []string{testRuleA},
+		},
+		{
+			name: "annotations with multiple terrascanSkipRules",
+			args: args{
+				annotations: map[string]interface{}{
+					terrascanSkip: []interface{}{testRuleA, testRuleB, testRuleC},
+				},
+			},
+			want: []string{testRuleA, testRuleB, testRuleC},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := readSkipRulesFromAnnotations(tt.args.annotations, tt.args.resourceID); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("readSkipRulesFromAnnotations() = got %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/iac-providers/kubernetes/v1/testdata/file-test-data/test_pod_skip_rules.yaml b/pkg/iac-providers/kubernetes/v1/testdata/file-test-data/test_pod_skip_rules.yaml
new file mode 100644
index 000000000..04ab9a462
--- /dev/null
+++ b/pkg/iac-providers/kubernetes/v1/testdata/file-test-data/test_pod_skip_rules.yaml
@@ -0,0 +1,37 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: myapp-pod-skip-rules
+  labels:
+    app: myapp
+    test: someupdate
+    test2: someupdate3
+  annotations:
+    terrascanSkip: [accurics.kubernetes.IAM.109]
+spec:
+  containers:
+  - name: myapp-container
+    image: busybox
+    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
+    securityContext:
+      allowPrivilegeEscalation: true
+---
+apiVersion: apps/v1beta1
+kind: Deployment
+metadata:
+  name: myapp-deployment-skip-rules
+  labels:
+    app: myapp
+    test: someupdate
+    test2: someupdate3
+  annotations:
+    terrascanSkip: [accurics.kubernetes.IAM.3, accurics.kubernetes.OPS.461]
+spec:
+  template:
+    spec:
+      containers:
+      - name: myapp-container2
+        image: busybox
+        command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
+        securityContext:
+          allowPrivilegeEscalation: true
diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/config1.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/config1.json
index 8c7f7cd8a..708d02fc2 100644
--- a/pkg/iac-providers/terraform/v12/testdata/tfjson/config1.json
+++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/config1.json
@@ -38,7 +38,8 @@
         "vpc_security_group_ids": [
           "${aws_security_group.sg_playground.id}"
         ]
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_internet_gateway": [
@@ -53,7 +54,8 @@
           "Environment": "${var.environment_tag}"
         },
         "vpc_id": "${aws_vpc.vpc_playground.id}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_key_pair": [
@@ -66,7 +68,8 @@
       "config": {
         "key_name": "testKey",
         "public_key": "${file(var.public_key_path)}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_route_table": [
@@ -87,7 +90,8 @@
           "Environment": "${var.environment_tag}"
         },
         "vpc_id": "${aws_vpc.vpc_playground.id}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_route_table_association": [
@@ -100,7 +104,8 @@
       "config": {
         "route_table_id": "${aws_route_table.rtb_public_playground.id}",
         "subnet_id": "${aws_subnet.subnet_public_playground.id}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_security_group": [
@@ -144,7 +149,8 @@
           "Environment": "${var.environment_tag}"
         },
         "vpc_id": "${aws_vpc.vpc_playground.id}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_subnet": [
@@ -161,7 +167,8 @@
           "Environment": "${var.environment_tag}"
         },
         "vpc_id": "${aws_vpc.vpc_playground.id}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "aws_vpc": [
@@ -178,7 +185,8 @@
         "tags": {
           "Environment": "${var.environment_tag}"
         }
-      }
+      },
+      "skip_rules": null
     }
   ]
 }
diff --git a/pkg/iac-providers/terraform/v12/testdata/tfjson/dummyconfig.json b/pkg/iac-providers/terraform/v12/testdata/tfjson/dummyconfig.json
index bbf33b988..8f39df6e0 100644
--- a/pkg/iac-providers/terraform/v12/testdata/tfjson/dummyconfig.json
+++ b/pkg/iac-providers/terraform/v12/testdata/tfjson/dummyconfig.json
@@ -14,7 +14,8 @@
           "profile": "${var.profile}",
           "region": "${var.region}"
         }
-      }
+      },
+      "skip_rules": null
     }
   ],
   "type1": [
@@ -39,7 +40,8 @@
         "test1": "hello",
         "test2": 5,
         "test3": "${1 + 2}"
-      }
+      },
+      "skip_rules": null
     }
   ],
   "type2": [
@@ -60,7 +62,8 @@
           "num": "${local.test2 + 5}",
           "thing": "${[for x in local.arr: x * 2]}"
         }
-      }
+      },
+      "skip_rules": null
     }
   ],
   "type3": [
@@ -75,7 +78,8 @@
         "heredoc": "This is a heredoc template.\nIt references ${local.other.3}\n",
         "heredoc2": "\t\tAnother heredoc, that\n\t\tdoesn't remove indentation\n\t\t${local.other.3}\n\t\t%{if true ? false : true}\"gotcha\"\\n%{else}4%{endif}\n",
         "simple": "${4 - 2}"
-      }
+      },
+      "skip_rules": null
     }
   ]
 }
diff --git a/pkg/utils/skip_rules_test.go b/pkg/utils/skip_rules_test.go
new file mode 100644
index 000000000..182c98aa1
--- /dev/null
+++ b/pkg/utils/skip_rules_test.go
@@ -0,0 +1,67 @@
+/*
+    Copyright (C) 2020 Accurics, Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+*/
+
+package utils
+
+import (
+	"reflect"
+	"testing"
+)
+
+// ---------------------- unit tests -------------------------------- //
+func TestGetSkipRules(t *testing.T) {
+
+	table := []struct {
+		name     string
+		input    string
+		expected []string
+	}{
+		{
+			name:  "no rules",
+			input: "no rules here",
+			// expected would be an empty slice of strings
+		},
+		{
+			name:     "single rule id",
+			input:    "#ts:skip=AWS.S3Bucket.DS.High.1041",
+			expected: []string{"AWS.S3Bucket.DS.High.1041"},
+		},
+		{
+			name:     "multiple comma separated no space",
+			input:    "#ts:skip=AWS.S3Bucket.DS.High.1041,AWS.S3Bucket.DS.High.1042",
+			expected: []string{"AWS.S3Bucket.DS.High.1041", "AWS.S3Bucket.DS.High.1042"},
+		},
+		{
+			name: "multiple comma separated random space",
+			input: "#ts:skip=  AWS.S3Bucket.DS.High.1041   ,		AWS.S3Bucket.DS.High.1042",
+			expected: []string{"AWS.S3Bucket.DS.High.1041", "AWS.S3Bucket.DS.High.1042"},
+		},
+		{
+			name:     "sample resource config",
+			input:    "{\n     #ts:skip=AWS.S3Bucket.DS.High.1041\n   region        = var.region\n   #ts:skip=AWS.S3Bucket.DS.High.1042 AWS.S3Bucket.DS.High.1043\n   bucket        = local.bucket_name\n   #ts:skip=AWS.S3Bucket.DS.High.1044,AWS.S3Bucket.DS.High.1045\n   force_destroy = true\n   #ts:skip= AWS.S3Bucket.DS.High.1046   ,   AWS.S3Bucket.DS.High.1047\n   acl           = \"public-read\"\n }",
+			expected: []string{"AWS.S3Bucket.DS.High.1041", "AWS.S3Bucket.DS.High.1042", "AWS.S3Bucket.DS.High.1044", "AWS.S3Bucket.DS.High.1045", "AWS.S3Bucket.DS.High.1046", "AWS.S3Bucket.DS.High.1047"},
+		},
+	}
+
+	for _, tt := range table {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := GetSkipRules(tt.input)
+			if !reflect.DeepEqual(actual, tt.expected) {
+				t.Errorf("rule ids got: '%v', want: '%v'", actual, tt.expected)
+			}
+		})
+	}
+}
diff --git a/pkg/writer/json_test.go b/pkg/writer/json_test.go
index c7a5325c7..824f69227 100644
--- a/pkg/writer/json_test.go
+++ b/pkg/writer/json_test.go
@@ -18,7 +18,8 @@ const (
       "config": {
         "bucket": "${module.m3.fullbucketname}",
         "policy": "${module.m2.fullbucketpolicy}"
-      }
+      },
+      "skip_rules": null
     }
   ]
 }`
diff --git a/pkg/writer/yaml_test.go b/pkg/writer/yaml_test.go
index 6c7daec7a..e7c34bd02 100644
--- a/pkg/writer/yaml_test.go
+++ b/pkg/writer/yaml_test.go
@@ -66,7 +66,8 @@ const (
       type: aws_s3_bucket
       config:
         bucket: ${module.m3.fullbucketname}
-        policy: ${module.m2.fullbucketpolicy}`
+        policy: ${module.m2.fullbucketpolicy}
+      skiprules: []`
 
 	scanTestOutputYAML = `results:
     violations: