From 725c14b7d89ea41ab8f3c73263ed568fa4d2abf6 Mon Sep 17 00:00:00 2001 From: Felipe Avelar Date: Mon, 19 Jul 2021 12:41:07 +0100 Subject: [PATCH] feat(parser): Added terraform functions on terraform parser - Parser evaluates standard functions from terraform - The list of functions that are evaluated can be found on `pkg/parser/terraform/functions/default.go`. This list only includes functions from terraform stdlib Signed-off-by: Felipe Avelar --- .gitignore | 3 + pkg/parser/terraform/converter/default.go | 55 ++++++++++++++++ .../terraform/converter/default_test.go | 59 +++++++++++++++++- pkg/parser/terraform/functions/default.go | 62 +++++++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 pkg/parser/terraform/functions/default.go diff --git a/.gitignore b/.gitignore index 406868b3150..070b259dab2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ results.* # coverage report coverage.html + +# Local files for tests +/local-files diff --git a/pkg/parser/terraform/converter/default.go b/pkg/parser/terraform/converter/default.go index 47e18fe9c45..40291acc6d4 100644 --- a/pkg/parser/terraform/converter/default.go +++ b/pkg/parser/terraform/converter/default.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/Checkmarx/kics/pkg/model" + "github.com/Checkmarx/kics/pkg/parser/terraform/functions" "github.com/getsentry/sentry-go" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -128,6 +129,8 @@ func (c *converter) convertExpression(expr hclsyntax.Expression) (interface{}, e return list, nil case *hclsyntax.ObjectConsExpr: return c.objectConsExpr(value) + case *hclsyntax.FunctionCallExpr: + return c.evalFunction(expr) default: // try to evaluate with variables valueConverted, _ := expr.Value(&hcl.EvalContext{ @@ -274,3 +277,55 @@ func (c *converter) wrapExpr(expr hclsyntax.Expression) (string, error) { } return "${" + expression + "}", nil } + +func (c *converter) evalFunction(expression hclsyntax.Expression) (interface{}, error) { + expressionEvaluated, err := expression.Value(&hcl.EvalContext{ + Variables: inputVarMap, + Functions: functions.TerraformFuncs, + }) + if err != nil { + for _, expressionError := range err { + if expressionError.Summary == "Unknown variable" { + jsonPath := c.rangeSource(expressionError.Expression.Range()) + rootKey := strings.Split(jsonPath, ".")[0] + if strings.Contains(jsonPath, ".") { + jsonCtyValue, convertErr := createEntryInputVar(strings.Split(jsonPath, ".")[1:], jsonPath) + if convertErr != nil { + return c.wrapExpr(expression) + } + inputVarMap[rootKey] = jsonCtyValue + } else { + inputVarMap[rootKey] = cty.StringVal(jsonPath) + } + } + } + expressionEvaluated, err = expression.Value(&hcl.EvalContext{ + Variables: inputVarMap, + Functions: functions.TerraformFuncs, + }) + if err != nil { + return c.wrapExpr(expression) + } + } + return ctyjson.SimpleJSONValue{Value: expressionEvaluated}, nil +} + +func createEntryInputVar(path []string, defaultValue string) (cty.Value, error) { + mapJSON := "{" + closeMap := "}" + for idx, key := range path { + if idx+1 < len(path) { + mapJSON += fmt.Sprintf("\"%s\":{", key) + closeMap += "}" + } else { + mapJSON += fmt.Sprintf("\"%s\": \"%s\"", key, defaultValue) + } + } + mapJSON += closeMap + jsonType, _ := ctyjson.ImpliedType([]byte(mapJSON)) + value, err := ctyjson.Unmarshal([]byte(mapJSON), jsonType) + if err != nil { + return cty.NilVal, err + } + return value, nil +} diff --git a/pkg/parser/terraform/converter/default_test.go b/pkg/parser/terraform/converter/default_test.go index fbd3ea6817e..186a319e09a 100644 --- a/pkg/parser/terraform/converter/default_test.go +++ b/pkg/parser/terraform/converter/default_test.go @@ -150,6 +150,57 @@ block "label_one" { } } +func TestEvalFunction(t *testing.T) { + type funcTest struct { + name string + input string + want string + wantErr bool + } + tests := []funcTest{ + { + name: "should evaluate without problems", + input: ` +block "label_one" { + policy = jsonencode({ + Id = "id" + }) + some_number = max(max(1,3),2) +} +`, + want: `{"block":{"label_one":{"policy":"{\"Id\":\"id\"}","some_number":3}}}`, + wantErr: false, + }, + { + name: "should evaluate after mocking variable", + input: ` +block "label_one" { + policy = jsonencode({ + Id = aws.meuId + }) + some_number = max(max(1,3),2) +} +`, + want: `{"block":{"label_one":{"policy":"{\"Id\":\"aws.meuId\"}","some_number":3}}}`, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file, _ := hclsyntax.ParseConfig([]byte(tt.input), "testFileName", hcl.Pos{Byte: 0, Line: 1, Column: 1}) + c := converter{bytes: file.Bytes} + got, err := c.convertBody(file.Body.(*hclsyntax.Body)) + fmt.Println(err) + require.True(t, (err != nil) == tt.wantErr) + gotJSON, _ := json.Marshal(got) + var wantJSON model.Document + _ = json.Unmarshal([]byte(tt.want), &wantJSON) + _ = json.Unmarshal(gotJSON, &got) + require.Equal(t, wantJSON, got) + }) + } +} + // TestLabelsWithNestedBlock tests the functions [DefaultConverted] and all the methods called by them func TestConversion(t *testing.T) { // nolint const input = ` @@ -201,6 +252,10 @@ data "terraform_remote_state" "remote" { bucket = "${var.bucket}-mybucket" key = "mykey" } + policy = jsonencode({ + Id = "MYBUCKETPOLICY" + }) + some_number = max(max(1,3),2) } variable "profile" {} variable "region" { @@ -257,7 +312,9 @@ variable "region" { "key": "mykey", "profile": "${var.profile}", "region": "us-east-1" - } + }, + "policy": "{\"Id\":\"MYBUCKETPOLICY\"}", + "some_number": 3 } } }, diff --git a/pkg/parser/terraform/functions/default.go b/pkg/parser/terraform/functions/default.go new file mode 100644 index 00000000000..e61d3dbece6 --- /dev/null +++ b/pkg/parser/terraform/functions/default.go @@ -0,0 +1,62 @@ +package functions + +import ( + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" +) + +// TerraformFuncs contains all functions, if KICS has to override a function +// it should create a file in this package and add/change this function key here +var TerraformFuncs = map[string]function.Function{ + "abs": stdlib.AbsoluteFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "chunklist": stdlib.ChunklistFunc, + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "log": stdlib.LogFunc, + "lower": stdlib.LowerFunc, + "max": stdlib.MaxFunc, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "parseint": stdlib.ParseIntFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regex": stdlib.RegexFunc, + "regexall": stdlib.RegexAllFunc, + "reverse": stdlib.ReverseListFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setunion": stdlib.SetUnionFunc, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "timeadd": stdlib.TimeAddFunc, + "title": stdlib.TitleFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "upper": stdlib.UpperFunc, + "values": stdlib.ValuesFunc, + "zipmap": stdlib.ZipmapFunc, +}