Repository: tenable/terrascan (language: python)

Issue:
Add support for YAML format for terrascan config file
It would be nice to support YAML format for terrascan config file. YAML format is widely adopted, simpler to use and a popular format with the kubernetes community. 


Discussion / hints:

Fix patch:
diff --git a/config/terrascan.toml b/config/terrascan.toml
index 045087c05..31ff68ea7 100644
--- a/config/terrascan.toml
+++ b/config/terrascan.toml
@@ -3,6 +3,8 @@
 # notifications configuration
 [notifications]
     [notifications.webhook]
+    type = "webhook"
+    [notifications.webhook.config]
     url = "https://httpbin.org/post"
 
 # scan and skip rules configuration
diff --git a/config/terrascan.yaml b/config/terrascan.yaml
new file mode 100644
index 000000000..342bff15e
--- /dev/null
+++ b/config/terrascan.yaml
@@ -0,0 +1,16 @@
+notifications:
+  webhook:
+    type: webhook
+    config:
+      url: 'https://httpbin.org/post'
+  webhook1:
+    type: webhook
+    config:
+      url: 'https://httpbin.org/post'
+rules:
+  scan-rules:
+    - AWS.S3Bucket.DS.High.1043
+    - AWS.S3Bucket.IAM.High.0370
+  skip-rules:
+    - AWS.S3Bucket.DS.High.1043
+    - AWS.S3Bucket.IAM.High.0370
diff --git a/pkg/config/config-reader.go b/pkg/config/config-reader.go
index 520e7b097..dd45a7c2f 100644
--- a/pkg/config/config-reader.go
+++ b/pkg/config/config-reader.go
@@ -20,9 +20,17 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 
 	"github.com/pelletier/go-toml"
 	"go.uber.org/zap"
+	"gopkg.in/yaml.v3"
+)
+
+const (
+	tomlExtension  = ".toml"
+	yamlExtension1 = ".yaml"
+	yamlExtension2 = ".yml"
 )
 
 var (
@@ -62,9 +70,21 @@ func NewTerrascanConfigReader(fileName string) (*TerrascanConfigReader, error) {
 		return configReader, ErrTomlLoadConfig
 	}
 
-	if err = toml.Unmarshal(data, &configReader.config); err != nil {
+	// check the extension of the file and decode using the file contents
+	// using the relevant package
+	switch filepath.Ext(fileName) {
+	case tomlExtension:
+		err = toml.Unmarshal(data, &configReader.config)
+	case yamlExtension1, yamlExtension2:
+		err = yaml.Unmarshal(data, &configReader.config)
+	default:
+		err = fmt.Errorf("file format %q not support for terrascan config file",
+			filepath.Ext(fileName))
+	}
+	if err != nil {
 		return configReader, err
 	}
+
 	return configReader, nil
 }
 
diff --git a/pkg/config/types.go b/pkg/config/types.go
index c5853a6f8..954a0dcc8 100644
--- a/pkg/config/types.go
+++ b/pkg/config/types.go
@@ -21,52 +21,52 @@ var global *TerrascanConfig
 
 // TerrascanConfig struct defines global variables/configurations across terrascan
 type TerrascanConfig struct {
-	Policy              `toml:"policy,omitempty"`
-	Notifications       map[string]Notifier `toml:"notifications,omitempty"`
-	Rules               `toml:"rules,omitempty"`
-	Category            `toml:"category,omitempty"`
-	Severity            `toml:"severity,omitempty"`
-	K8sAdmissionControl `toml:"k8s-admission-control,omitempty"`
+	Policy              `toml:"policy,omitempty" yaml:"policy,omitempty"`
+	Notifications       map[string]Notifier `toml:"notifications,omitempty" yaml:"notifications,omitempty"`
+	Rules               `toml:"rules,omitempty" yaml:"rules,omitempty"`
+	Category            `toml:"category,omitempty" yaml:"category,omitempty"`
+	Severity            `toml:"severity,omitempty" yaml:"severity,omitempty"`
+	K8sAdmissionControl `toml:"k8s-admission-control,omitempty" yaml:"k8s-admission-control,omitempty"`
 }
 
 // Category defines the categories of violations that you want to be reported
 type Category struct {
-	List []string `toml:"list"`
+	List []string `toml:"list" yaml:"list"`
 }
 
 // Severity defines the minimum level of severity of violations that you want to be reported
 type Severity struct {
-	Level string `toml:"level"`
+	Level string `toml:"level" yaml:"level"`
 }
 
 // Policy struct defines policy specific configurations
 type Policy struct {
 	// policy repo local path
-	BasePath string `toml:"path,omitempty"`
+	BasePath string `toml:"path,omitempty" yaml:"path,omitempty"`
 	// local filepath where repository containing policies is cached at
-	RepoPath string `toml:"rego_subdir,omitempty"`
+	RepoPath string `toml:"rego_subdir,omitempty" yaml:"rego_subdir,omitempty"`
 
 	// policy git url and branch
-	RepoURL string `toml:"repo_url,omitempty"`
-	Branch  string `toml:"branch,omitempty"`
+	RepoURL string `toml:"repo_url,omitempty" yaml:"repo_url,omitempty"`
+	Branch  string `toml:"branch,omitempty" yaml:"branch,omitempty"`
 }
 
 // Notifier represent a single notification in the terrascan config file
 type Notifier struct {
-	NotifierType   string      `toml:"type"`
-	NotifierConfig interface{} `toml:"config"`
+	NotifierType   string      `toml:"type" yaml:"type"`
+	NotifierConfig interface{} `toml:"config" yaml:"config"`
 }
 
 // Rules represents scan and skip rules in the terrascan config file
 type Rules struct {
-	ScanRules []string `toml:"scan-rules,omitempty"`
-	SkipRules []string `toml:"skip-rules,omitempty"`
+	ScanRules []string `toml:"scan-rules,omitempty" yaml:"scan-rules,omitempty"`
+	SkipRules []string `toml:"skip-rules,omitempty" yaml:"skip-rules,omitempty"`
 }
 
 // K8sAdmissionControl deny rules in the terrascan config file
 type K8sAdmissionControl struct {
-	Dashboard      bool     `toml:"dashboard,omitempty"`
-	DeniedSeverity string   `toml:"denied-severity,omitempty"`
-	Categories     []string `toml:"denied-categories,omitempty"`
-	SaveRequests   bool     `toml:"save-requests,omitempty"`
+	Dashboard      bool     `toml:"dashboard,omitempty" yaml:"dashboard,omitempty"`
+	DeniedSeverity string   `toml:"denied-severity,omitempty" yaml:"denied-severity,omitempty"`
+	Categories     []string `toml:"denied-categories,omitempty" yaml:"denied-categories,omitempty"`
+	SaveRequests   bool     `toml:"save-requests,omitempty" yaml:"save-requests,omitempty"`
 }


Test patch:
diff --git a/pkg/config/config-reader_test.go b/pkg/config/config-reader_test.go
index 931c83366..845051353 100644
--- a/pkg/config/config-reader_test.go
+++ b/pkg/config/config-reader_test.go
@@ -71,6 +71,14 @@ func TestNewTerrascanConfigReader(t *testing.T) {
 			wantErr: true,
 			want:    &TerrascanConfigReader{},
 		},
+		{
+			name: "invalid config file format",
+			args: args{
+				fileName: "test.invalid",
+			},
+			wantErr: true,
+			want:    &TerrascanConfigReader{},
+		},
 		{
 			name: "invalid toml config file",
 			args: args{
@@ -79,6 +87,14 @@ func TestNewTerrascanConfigReader(t *testing.T) {
 			wantErr: true,
 			want:    &TerrascanConfigReader{},
 		},
+		{
+			name: "invalid yaml config file",
+			args: args{
+				fileName: "testdata/invalid.yaml",
+			},
+			wantErr: true,
+			want:    &TerrascanConfigReader{},
+		},
 		{
 			name: "valid toml config file with partial fields",
 			args: args{
@@ -155,6 +171,79 @@ func TestNewTerrascanConfigReader(t *testing.T) {
 			Policy: testPolicy,
 			Rules:  testRules,
 		},
+		{
+			name: "invalid yaml config file",
+			args: args{
+				fileName: "testdata/invalid.yaml",
+			},
+			wantErr: true,
+			want:    &TerrascanConfigReader{},
+		},
+		{
+			name: "valid yaml config file with all fields",
+			args: args{
+				fileName: "testdata/terrascan-config-all-fields.yaml",
+			},
+			want: &TerrascanConfigReader{
+				config: TerrascanConfig{
+					Policy: testPolicy,
+					Notifications: map[string]Notifier{
+						"webhook1": testNotifier,
+					},
+					Rules: testRules,
+				},
+			},
+			assertGetters: true,
+			notifications: map[string]Notifier{
+				"webhook1": testNotifier,
+			},
+			Policy: testPolicy,
+			Rules:  testRules,
+		},
+		{
+			name: "valid yaml config file with all fields and severity defined",
+			args: args{
+				fileName: "testdata/terrascan-config-severity.yml",
+			},
+			want: &TerrascanConfigReader{
+				config: TerrascanConfig{
+					Policy: testPolicy,
+					Notifications: map[string]Notifier{
+						"webhook1": testNotifier,
+					},
+					Rules:    testRules,
+					Severity: highSeverity,
+				},
+			},
+			assertGetters: true,
+			notifications: map[string]Notifier{
+				"webhook1": testNotifier,
+			},
+			Policy: testPolicy,
+			Rules:  testRules,
+		},
+		{
+			name: "valid yaml config file with all fields and categories defined",
+			args: args{
+				fileName: "testdata/terrascan-config-category.yaml",
+			},
+			want: &TerrascanConfigReader{
+				config: TerrascanConfig{
+					Policy: testPolicy,
+					Notifications: map[string]Notifier{
+						"webhook1": testNotifier,
+					},
+					Rules:    testRules,
+					Category: categoryList,
+				},
+			},
+			assertGetters: true,
+			notifications: map[string]Notifier{
+				"webhook1": testNotifier,
+			},
+			Policy: testPolicy,
+			Rules:  testRules,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/config/testdata/invalid.yaml b/pkg/config/testdata/invalid.yaml
new file mode 100644
index 000000000..8b0ce0c41
--- /dev/null
+++ b/pkg/config/testdata/invalid.yaml
@@ -0,0 +1,1 @@
+invalid yaml file
\ No newline at end of file
diff --git a/pkg/config/testdata/terrascan-config-all-fields.yaml b/pkg/config/testdata/terrascan-config-all-fields.yaml
new file mode 100644
index 000000000..8d91676f5
--- /dev/null
+++ b/pkg/config/testdata/terrascan-config-all-fields.yaml
@@ -0,0 +1,19 @@
+policy:
+  path: custom-path
+  rego_subdir: rego-subdir
+  repo_url: 'https://repository/url'
+  branch: branch-name
+notifications:
+  webhook1:
+    type: webhook
+    config:
+      url: testurl1
+rules:
+  scan-rules:
+    - rule.1
+    - rule.2
+    - rule.3
+    - rule.4
+    - rule.5
+  skip-rules:
+    - rule.1
diff --git a/pkg/config/testdata/terrascan-config-category.yaml b/pkg/config/testdata/terrascan-config-category.yaml
new file mode 100644
index 000000000..390bbb8db
--- /dev/null
+++ b/pkg/config/testdata/terrascan-config-category.yaml
@@ -0,0 +1,23 @@
+policy:
+  path: custom-path
+  rego_subdir: rego-subdir
+  repo_url: 'https://repository/url'
+  branch: branch-name
+notifications:
+  webhook1:
+    type: webhook
+    config:
+      url: testurl1
+rules:
+  scan-rules:
+    - rule.1
+    - rule.2
+    - rule.3
+    - rule.4
+    - rule.5
+  skip-rules:
+    - rule.1
+category:
+  list:
+    - category.1
+    - category.2
diff --git a/pkg/config/testdata/terrascan-config-severity.yml b/pkg/config/testdata/terrascan-config-severity.yml
new file mode 100644
index 000000000..f25954c7b
--- /dev/null
+++ b/pkg/config/testdata/terrascan-config-severity.yml
@@ -0,0 +1,21 @@
+policy:
+  path: custom-path
+  rego_subdir: rego-subdir
+  repo_url: 'https://repository/url'
+  branch: branch-name
+notifications:
+  webhook1:
+    type: webhook
+    config:
+      url: testurl1
+rules:
+  scan-rules:
+    - rule.1
+    - rule.2
+    - rule.3
+    - rule.4
+    - rule.5
+  skip-rules:
+    - rule.1
+severity:
+  level: high