Repository: aquasecurity/tfsec (go)

Issue:
Ignore checks per workspace
**Is your feature request related to a problem? Please describe.**
There is no way to ignore a check for a specific workspace.

**Describe the solution you'd like**
Same as the `--tfvars-file`, I'd like to be able to ignore a test in an environment but not on another maybe via flags or by taking into account the current workspace.

**Describe alternatives you've considered**
No alternatives have been considered.

**Additional context**
It's common to use workspaces to manage different environments. One may want to ignore a test (i.e.`GCP009`) in staging but not in production.

Right now, by adding the (i.e.) `#tfsec:ignore:GCP009` to the tf code it'll be applied to every workspace, creating a situation where you are forced to have a staging/production parity environment, which not everyone wants to.


Pull request description:
Add support for the workspace ignores
Pass the --workspace argument at runtime
ignores should include #tfsec:ignore:AWS006:ws:workspaceName

Resolves #907


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8d8c97610..9f6b091ab 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,39 +1,49 @@
 {
-    "version": "0.2.0",
-    "configurations": [
-
-
-        {
-            "name": "Basic tfsec",
-            "type": "go",
-            "request": "launch",
-            "mode": "auto",
-            "program": "${workspaceFolder}/cmd/tfsec/main.go",
-            "args": ["${workspaceFolder}/example"]
-        },
-        {
-            "name": "custom_functions test",
-            "type": "go",
-            "request": "launch",
-            "mode": "auto",
-            "program": "${workspaceFolder}/cmd/tfsec/main.go",
-            "args": ["${workspaceFolder}/example/custom_functions"]
-        },
-        {
-            "name": "foreach test",
-            "type": "go",
-            "request": "launch",
-            "mode": "auto",
-            "program": "${workspaceFolder}/cmd/tfsec/main.go",
-            "args": ["${workspaceFolder}/example/foreach"]
-        },
-        {
-            "name": "foreach module test",
-            "type": "go",
-            "request": "launch",
-            "mode": "auto",
-            "program": "${workspaceFolder}/cmd/tfsec/main.go",
-            "args": ["${workspaceFolder}/example/foreach-module/src"]
-        }
-    ]
-}
\ No newline at end of file
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Basic tfsec",
+      "type": "go",
+      "request": "launch",
+      "mode": "auto",
+      "program": "${workspaceFolder}/cmd/tfsec/main.go",
+      "args": ["${workspaceFolder}/example"]
+    },
+    {
+      "name": "custom_functions test",
+      "type": "go",
+      "request": "launch",
+      "mode": "auto",
+      "program": "${workspaceFolder}/cmd/tfsec/main.go",
+      "args": ["${workspaceFolder}/example/custom_functions"]
+    },
+    {
+      "name": "foreach test",
+      "type": "go",
+      "request": "launch",
+      "mode": "auto",
+      "program": "${workspaceFolder}/cmd/tfsec/main.go",
+      "args": ["${workspaceFolder}/example/foreach"]
+    },
+    {
+      "name": "foreach module test",
+      "type": "go",
+      "request": "launch",
+      "mode": "auto",
+      "program": "${workspaceFolder}/cmd/tfsec/main.go",
+      "args": ["${workspaceFolder}/example/foreach-module/src"]
+    },
+    {
+      "name": "workspace test",
+      "type": "go",
+      "request": "launch",
+      "mode": "auto",
+      "program": "${workspaceFolder}/cmd/tfsec/main.go",
+      "args": [
+        "${workspaceFolder}/example/workspace",
+        "--workspace",
+        "somethingelse"
+      ]
+    }
+  ]
+}
diff --git a/cmd/tfsec/main.go b/cmd/tfsec/main.go
index eac21227b..693b4fdb8 100644
--- a/cmd/tfsec/main.go
+++ b/cmd/tfsec/main.go
@@ -55,6 +55,7 @@ var allDirs = false
 var runStatistics bool
 var ignoreHCLErrors bool
 var stopOnCheckError bool
+var workspace string
 
 func init() {
 	rootCmd.Flags().BoolVar(&ignoreHCLErrors, "ignore-hcl-errors", ignoreHCLErrors, "Stop and report an error if an HCL parse error is encountered")
@@ -81,6 +82,7 @@ func init() {
 	rootCmd.Flags().BoolVar(&ignoreWarnings, "ignore-warnings", ignoreWarnings, "[DEPRECATED] Don't show warnings in the output.")
 	rootCmd.Flags().BoolVar(&ignoreInfo, "ignore-info", ignoreWarnings, "[DEPRECATED] Don't show info results in the output.")
 	rootCmd.Flags().BoolVarP(&stopOnCheckError, "allow-checks-to-panic", "p", stopOnCheckError, "Allow panics to propagate up from rule checking")
+	rootCmd.Flags().StringVarP(&workspace, "workspace", "w", workspace, "Specify a workspace for ignore limits")
 }
 
 func main() {
@@ -287,6 +289,11 @@ func getParserOptions() []parser.Option {
 	if !ignoreHCLErrors {
 		opts = append(opts, parser.OptionStopOnHCLError())
 	}
+
+	if workspace != "" {
+		opts = append(opts, parser.OptionWithWorkspaceName(workspace))
+	}
+
 	return opts
 }
 
@@ -352,6 +359,9 @@ func getScannerOptions() []scanner.Option {
 	if includeIgnored {
 		options = append(options, scanner.OptionIncludeIgnored())
 	}
+	if workspace != "" {
+		options = append(options, scanner.OptionWithWorkspaceName(workspace))
+	}
 
 	options = append(options, scanner.OptionIgnoreCheckErrors(!stopOnCheckError))
 
diff --git a/example/moduleWithIgnore/main.tf b/example/moduleWithIgnore/main.tf
index 050c03c42..c667b64f5 100644
--- a/example/moduleWithIgnore/main.tf
+++ b/example/moduleWithIgnore/main.tf
@@ -1,6 +1,6 @@
-#tfsec:ignore:AWS052:exp:2021-02-02
+#tfsec:ignore:AWS052:ws:testworkspace:exp:2021-02-02
 module "db" {
-  source = "terraform-aws-modules/rds/aws"
+  source  = "terraform-aws-modules/rds/aws"
   version = "~> 2.0"
 
   identifier = "demodb"
@@ -20,7 +20,7 @@ module "db" {
 }
 
 resource "aws_ssm_parameter" "pw" {
-  name = "pw"
-  type = "SecureString"
+  name  = "pw"
+  type  = "SecureString"
   value = "changeme"
-}
\ No newline at end of file
+}
diff --git a/example/workspace/main.tf b/example/workspace/main.tf
new file mode 100644
index 000000000..6f4e30724
--- /dev/null
+++ b/example/workspace/main.tf
@@ -0,0 +1,13 @@
+resource "aws_instance" "compliant" {
+
+  count = terraform.workspace == "default" ? 5 : 20
+
+  ami            = "ami-12345"
+  instance_type  = "t2.small"
+  cpu_core_count = 4
+
+  tags = {
+    Department = "Finance"
+    CostCentre = "CC1234"
+  }
+}
diff --git a/internal/app/tfsec/parser/evaluator.go b/internal/app/tfsec/parser/evaluator.go
index cdb9783c8..ccfcb473c 100644
--- a/internal/app/tfsec/parser/evaluator.go
+++ b/internal/app/tfsec/parser/evaluator.go
@@ -413,3 +413,9 @@ func (e *Evaluator) getValuesByBlockType(blockType string) cty.Value {
 	return cty.ObjectVal(values)
 
 }
+
+func (e *Evaluator) SetWorkspace(workspaceName string) {
+
+	ctyWorkspaceName := cty.StringVal(workspaceName)
+	e.ctx.Variables["terraform"] = cty.ObjectVal(map[string]cty.Value{"workspace": ctyWorkspaceName})
+}
diff --git a/internal/app/tfsec/parser/option.go b/internal/app/tfsec/parser/option.go
index 7663b79e8..ba4dc0dc3 100644
--- a/internal/app/tfsec/parser/option.go
+++ b/internal/app/tfsec/parser/option.go
@@ -19,3 +19,9 @@ func OptionStopOnHCLError() Option {
 		p.stopOnHCLError = true
 	}
 }
+
+func OptionWithWorkspaceName(workspaceName string) Option {
+	return func(p *Parser) {
+		p.workspaceName = workspaceName
+	}
+}
diff --git a/internal/app/tfsec/parser/parser.go b/internal/app/tfsec/parser/parser.go
index 1188ea3cc..5948a5ca4 100644
--- a/internal/app/tfsec/parser/parser.go
+++ b/internal/app/tfsec/parser/parser.go
@@ -20,6 +20,7 @@ type Parser struct {
 	tfvarsPaths    []string
 	stopOnFirstTf  bool
 	stopOnHCLError bool
+	workspaceName  string
 }
 
 // New creates a new Parser
@@ -27,6 +28,7 @@ func New(initialPath string, options ...Option) *Parser {
 	p := &Parser{
 		initialPath:   initialPath,
 		stopOnFirstTf: true,
+		workspaceName: "default",
 	}
 
 	for _, option := range options {
@@ -100,6 +102,7 @@ func (parser *Parser) ParseDirectory() (block.Blocks, error) {
 
 	debug.Log("Evaluating expressions...")
 	evaluator := NewEvaluator(tfPath, tfPath, blocks, inputVars, modulesMetadata, nil, parser.stopOnHCLError)
+	evaluator.SetWorkspace(parser.workspaceName)
 	evaluatedBlocks, err := evaluator.EvaluateAll()
 	if err != nil {
 		return nil, err
diff --git a/internal/app/tfsec/scanner/option.go b/internal/app/tfsec/scanner/option.go
index 9b71c4d1f..e40f22c1f 100644
--- a/internal/app/tfsec/scanner/option.go
+++ b/internal/app/tfsec/scanner/option.go
@@ -25,3 +25,9 @@ func OptionIgnoreCheckErrors(ignore bool) func(s *Scanner) {
 		s.ignoreCheckErrors = ignore
 	}
 }
+
+func OptionWithWorkspaceName(workspaceName string) func(s *Scanner) {
+	return func(s *Scanner) {
+		s.workspaceName = workspaceName
+	}
+}
diff --git a/internal/app/tfsec/scanner/scanner.go b/internal/app/tfsec/scanner/scanner.go
index d6cf5bdc0..36821bfda 100644
--- a/internal/app/tfsec/scanner/scanner.go
+++ b/internal/app/tfsec/scanner/scanner.go
@@ -25,6 +25,7 @@ type Scanner struct {
 	includeIgnored    bool
 	excludedRuleIDs   []string
 	ignoreCheckErrors bool
+	workspaceName     string
 }
 
 // New creates a new Scanner
@@ -153,12 +154,44 @@ func (scanner *Scanner) checkRangeIgnored(id string, r block.Range, b block.Bloc
 	}
 
 	if foundValidIgnore {
-		return isIgnoreWithinExpiry(ignoreLine, id, b.Range())
+		return scanner.verifyIgnoreConditions(ignoreLine, id, b)
 	}
 
 	return foundValidIgnore
 }
 
+func (scanner *Scanner) verifyIgnoreConditions(ignoreLine string, id string, b block.Block) bool {
+	for _, ignoreCode := range strings.Split(ignoreLine, " ") {
+		ignoreCode = fmt.Sprintf("%s:", ignoreCode)
+		if strings.Contains(ignoreCode, fmt.Sprintf("%s:", id)) || strings.Contains(ignoreCode, "ignore:*") {
+			if scanner.workspaceName != "" && strings.Contains(ignoreCode, ":ws:") {
+				if isIgnoreWithinExpiry(ignoreCode, id, b.Range()) {
+					if scanner.isWorkspaceIgnored(ignoreCode, id, b.Range()) {
+						return true
+					} else {
+						return false
+					}
+				}
+			} else {
+				return isIgnoreWithinExpiry(ignoreCode, id, b.Range())
+			}
+		}
+	}
+	return false
+}
+
+func (s *Scanner) isWorkspaceIgnored(ignoreLine string, id string, r block.Range) bool {
+	ignoreLine = fmt.Sprintf("%s:", ignoreLine)
+	if strings.Contains(ignoreLine, ":ws:") {
+		if s.workspaceName == "" {
+			return false
+		}
+		return strings.Contains(ignoreLine, fmt.Sprintf(":ws:%s:", s.workspaceName))
+	}
+
+	return false
+}
+
 func isIgnoreWithinExpiry(ignoreLine string, id string, r block.Range) bool {
 	expWithCode := fmt.Sprintf("%s:exp:", id)
 	if indexExpFound := strings.Index(ignoreLine, expWithCode); indexExpFound > 0 {


Test patch:
diff --git a/internal/app/tfsec/test/ignore_test.go b/internal/app/tfsec/test/ignore_test.go
index c071eb7ca..37f08f686 100644
--- a/internal/app/tfsec/test/ignore_test.go
+++ b/internal/app/tfsec/test/ignore_test.go
@@ -169,3 +169,27 @@ resource "aws_security_group_rule" "my-rule" {
 `, t)
 	assert.Len(t, results, 0)
 }
+
+func Test_IgnoreIgnoreWithExpiryAndWorkspaceAndWorkspaceSupplied(t *testing.T) {
+	results := testutil.ScanHCL(`
+# tfsec:ignore:AWS006:exp:2221-01-02 #tfsec:ignore:AWS018:ws:testworkspace
+resource "aws_security_group_rule" "my-rule" {
+    type        = "ingress"
+	
+    cidr_blocks = ["0.0.0.0/0"]
+}
+`, t, scanner.OptionWithWorkspaceName("testworkspace"))
+	assert.Len(t, results, 0)
+}
+
+func Test_IgnoreIgnoreWithExpiryAndWorkspaceButWrongWorkspaceSupplied(t *testing.T) {
+	results := testutil.ScanHCL(`
+# tfsec:ignore:AWS006:exp:2221-01-02 #tfsec:ignore:AWS018:ws:otherworkspace
+resource "aws_security_group_rule" "my-rule" {
+    type        = "ingress"
+	
+    cidr_blocks = ["0.0.0.0/0"]
+}
+`, t, scanner.OptionWithWorkspaceName("testworkspace"))
+	assert.Len(t, results, 1)
+}
diff --git a/internal/app/tfsec/testutil/util.go b/internal/app/tfsec/testutil/util.go
index 8e8ad3b92..620641971 100644
--- a/internal/app/tfsec/testutil/util.go
+++ b/internal/app/tfsec/testutil/util.go
@@ -17,9 +17,13 @@ import (
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 )
 
-func ScanHCL(source string, t *testing.T) []result.Result {
+func ScanHCL(source string, t *testing.T, additionalOptions ...scanner.Option) []result.Result {
 	blocks := CreateBlocksFromSource(source, ".tf", t)
-	return scanner.New(scanner.OptionIgnoreCheckErrors(false)).Scan(blocks)
+	scanner := scanner.New(scanner.OptionIgnoreCheckErrors(false))
+	for _, opt := range additionalOptions {
+		opt(scanner)
+	}
+	return scanner.Scan(blocks)
 }
 
 func ScanJSON(source string, t *testing.T) []result.Result {