From 2eae365bef7a6fa2770588d38ede8be12c68b462 Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Fri, 29 Mar 2024 16:31:42 +0700 Subject: [PATCH 1/3] refactor(terraform): sync funcs with Terraform --- .../scanners/terraform/parser/funcs/cidr.go | 57 ++-- .../terraform/parser/funcs/collection.go | 105 ++----- .../terraform/parser/funcs/conversion.go | 135 +------- .../scanners/terraform/parser/funcs/crypto.go | 103 ++----- .../terraform/parser/funcs/datetime.go | 140 +++++++-- .../terraform/parser/funcs/defaults.go | 289 ------------------ .../terraform/parser/funcs/encoding.go | 112 ++----- .../terraform/parser/funcs/filesystem.go | 84 +---- pkg/iac/scanners/terraform/parser/funcs/ip.go | 261 ++++++++++++++++ .../scanners/terraform/parser/funcs/marks.go | 4 +- .../scanners/terraform/parser/funcs/number.go | 63 ++-- .../scanners/terraform/parser/funcs/redact.go | 20 ++ .../terraform/parser/funcs/refinements.go | 10 + .../terraform/parser/funcs/sensitive.go | 33 +- .../scanners/terraform/parser/funcs/string.go | 114 ++++++- .../scanners/terraform/parser/functions.go | 111 ++++--- 16 files changed, 722 insertions(+), 919 deletions(-) delete mode 100644 pkg/iac/scanners/terraform/parser/funcs/defaults.go create mode 100644 pkg/iac/scanners/terraform/parser/funcs/ip.go create mode 100644 pkg/iac/scanners/terraform/parser/funcs/redact.go create mode 100644 pkg/iac/scanners/terraform/parser/funcs/refinements.go diff --git a/pkg/iac/scanners/terraform/parser/funcs/cidr.go b/pkg/iac/scanners/terraform/parser/funcs/cidr.go index 5f1504c0a8a1..910f653779ec 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/cidr.go +++ b/pkg/iac/scanners/terraform/parser/funcs/cidr.go @@ -4,7 +4,6 @@ package funcs import ( "fmt" "math/big" - "net" "github.com/apparentlymart/go-cidr/cidr" "github.com/zclconf/go-cty/cty" @@ -12,7 +11,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -// CidrHostFunc constructs a function that calculates a full host IP address +// CidrHostFunc contructs a function that calculates a full host IP address // within a given IP network address prefix. var CidrHostFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -25,13 +24,14 @@ var CidrHostFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var hostNum *big.Int if err := gocty.FromCtyValue(args[1], &hostNum); err != nil { return cty.UnknownVal(cty.String), err } - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } @@ -45,7 +45,7 @@ var CidrHostFunc = function.New(&function.Spec{ }, }) -// CidrNetmaskFunc constructs a function that converts an IPv4 address prefix given +// CidrNetmaskFunc contructs a function that converts an IPv4 address prefix given // in CIDR notation into a subnet mask address. var CidrNetmaskFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -54,18 +54,23 @@ var CidrNetmaskFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } - return cty.StringVal(net.IP(network.Mask).String()), nil + if network.IP.To4() == nil { + return cty.UnknownVal(cty.String), fmt.Errorf("IPv6 addresses cannot have a netmask: %s", args[0].AsString()) + } + + return cty.StringVal(IP(network.Mask).String()), nil }, }) -// CidrSubnetFunc constructs a function that calculates a subnet address within +// CidrSubnetFunc contructs a function that calculates a subnet address within // a given IP network address prefix. var CidrSubnetFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -82,7 +87,8 @@ var CidrSubnetFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var newbits int if err := gocty.FromCtyValue(args[1], &newbits); err != nil { @@ -93,7 +99,7 @@ var CidrSubnetFunc = function.New(&function.Spec{ return cty.UnknownVal(cty.String), err } - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } @@ -120,9 +126,10 @@ var CidrSubnetsFunc = function.New(&function.Spec{ Name: "newbits", Type: cty.Number, }, - Type: function.StaticReturnType(cty.List(cty.String)), + Type: function.StaticReturnType(cty.List(cty.String)), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err) } @@ -186,27 +193,3 @@ var CidrSubnetsFunc = function.New(&function.Spec{ return cty.ListVal(retVals), nil }, }) - -// CidrHost calculates a full host IP address within a given IP network address prefix. -func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) { - return CidrHostFunc.Call([]cty.Value{prefix, hostnum}) -} - -// CidrNetmask converts an IPv4 address prefix given in CIDR notation into a subnet mask address. -func CidrNetmask(prefix cty.Value) (cty.Value, error) { - return CidrNetmaskFunc.Call([]cty.Value{prefix}) -} - -// CidrSubnet calculates a subnet address within a given IP network address prefix. -func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) { - return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum}) -} - -// CidrSubnets calculates a sequence of consecutive subnet prefixes that may -// be of different prefix lengths under a common base prefix. -func CidrSubnets(prefix cty.Value, newbits ...cty.Value) (cty.Value, error) { - args := make([]cty.Value, len(newbits)+1) - args[0] = prefix - copy(args[1:], newbits) - return CidrSubnetsFunc.Call(args) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/collection.go b/pkg/iac/scanners/terraform/parser/funcs/collection.go index f68af2ce36af..d5deb65a68e5 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/collection.go +++ b/pkg/iac/scanners/terraform/parser/funcs/collection.go @@ -33,6 +33,7 @@ var LengthFunc = function.New(&function.Spec{ return cty.Number, errors.New("argument must be a string, a collection type, or a structural type") } }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { coll := args[0] collTy := args[0].Type() @@ -69,7 +70,8 @@ var AllTrueFunc = function.New(&function.Spec{ Type: cty.List(cty.Bool), }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result := cty.True for it := args[0].ElementIterator(); it.Next(); { @@ -98,7 +100,8 @@ var AnyTrueFunc = function.New(&function.Spec{ Type: cty.List(cty.Bool), }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result := cty.False var hasUnknown bool @@ -147,6 +150,7 @@ var CoalesceFunc = function.New(&function.Spec{ } return retType, nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { for _, argVal := range args { // We already know this will succeed because of the checks in our Type func above @@ -179,7 +183,8 @@ var IndexFunc = function.New(&function.Spec{ Type: cty.DynamicPseudoType, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) { return cty.NilVal, errors.New("argument must be a list or tuple") @@ -312,8 +317,8 @@ var LookupFunc = function.New(&function.Spec{ return defaultVal.WithMarks(markses...), nil } - return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf( - "lookup failed to find '%s'", lookupKey) + return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf( + "lookup failed to find key %s", redactIfSensitive(lookupKey, keyMarks)) }, }) @@ -344,6 +349,7 @@ var MatchkeysFunc = function.New(&function.Spec{ // the return type is based on args[0] (values) return args[0].Type(), nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !args[0].IsKnown() { return cty.UnknownVal(cty.List(retType.ElementType())), nil @@ -353,7 +359,7 @@ var MatchkeysFunc = function.New(&function.Spec{ return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal") } - var output []cty.Value + output := make([]cty.Value, 0) values := args[0] // Keys and searchset must be the same type. @@ -487,7 +493,8 @@ var SumFunc = function.New(&function.Spec{ Type: cty.DynamicPseudoType, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !args[0].CanIterateElements() { @@ -528,6 +535,10 @@ var SumFunc = function.New(&function.Spec{ if s.IsNull() { return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") } + s, err = convert.Convert(s, cty.Number) + if err != nil { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } for _, v := range arg[1:] { if v.IsNull() { return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") @@ -552,7 +563,8 @@ var TransposeFunc = function.New(&function.Spec{ Type: cty.Map(cty.List(cty.String)), }, }, - Type: function.StaticReturnType(cty.Map(cty.List(cty.String))), + Type: function.StaticReturnType(cty.Map(cty.List(cty.String))), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { inputMap := args[0] if !inputMap.IsWhollyKnown() { @@ -582,7 +594,7 @@ var TransposeFunc = function.New(&function.Spec{ } for outKey, outVal := range tmpMap { - var values []cty.Value + values := make([]cty.Value, 0) for _, v := range outVal { values = append(values, cty.StringVal(v)) } @@ -600,7 +612,7 @@ var TransposeFunc = function.New(&function.Spec{ // ListFunc constructs a function that takes an arbitrary number of arguments // and returns a list containing those values in the same order. // -// Deprecated: This function is deprecated in Terraform v0.12 +// This function is deprecated in Terraform v0.12 var ListFunc = function.New(&function.Spec{ Params: []function.Parameter{}, VarParam: &function.Parameter{ @@ -621,7 +633,7 @@ var ListFunc = function.New(&function.Spec{ // MapFunc constructs a function that takes an even number of arguments and // returns a map whose elements are constructed from consecutive pairs of arguments. // -// Deprecated: This function is deprecated in Terraform v0.12 +// This function is deprecated in Terraform v0.12 var MapFunc = function.New(&function.Spec{ Params: []function.Parameter{}, VarParam: &function.Parameter{ @@ -638,74 +650,3 @@ var MapFunc = function.New(&function.Spec{ return cty.DynamicVal, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map") }, }) - -// Length returns the number of elements in the given collection or number of -// Unicode characters in the given string. -func Length(collection cty.Value) (cty.Value, error) { - return LengthFunc.Call([]cty.Value{collection}) -} - -// AllTrue returns true if all elements of the list are true. If the list is empty, -// return true. -func AllTrue(collection cty.Value) (cty.Value, error) { - return AllTrueFunc.Call([]cty.Value{collection}) -} - -// AnyTrue returns true if any element of the list is true. If the list is empty, -// return false. -func AnyTrue(collection cty.Value) (cty.Value, error) { - return AnyTrueFunc.Call([]cty.Value{collection}) -} - -// Coalesce takes any number of arguments and returns the first one that isn't empty. -func Coalesce(args ...cty.Value) (cty.Value, error) { - return CoalesceFunc.Call(args) -} - -// Index finds the element index for a given value in a list. -func Index(list, value cty.Value) (cty.Value, error) { - return IndexFunc.Call([]cty.Value{list, value}) -} - -// List takes any number of list arguments and returns a list containing those -// -// values in the same order. -func List(args ...cty.Value) (cty.Value, error) { - return ListFunc.Call(args) -} - -// Lookup performs a dynamic lookup into a map. -// There are two required arguments, map and key, plus an optional default, -// which is a value to return if no key is found in map. -func Lookup(args ...cty.Value) (cty.Value, error) { - return LookupFunc.Call(args) -} - -// Map takes an even number of arguments and returns a map whose elements are constructed -// from consecutive pairs of arguments. -func Map(args ...cty.Value) (cty.Value, error) { - return MapFunc.Call(args) -} - -// Matchkeys constructs a new list by taking a subset of elements from one list -// whose indexes match the corresponding indexes of values in another list. -func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) { - return MatchkeysFunc.Call([]cty.Value{values, keys, searchset}) -} - -// One returns either the first element of a one-element list, or null -// if given a zero-element list.. -func One(list cty.Value) (cty.Value, error) { - return OneFunc.Call([]cty.Value{list}) -} - -// Sum adds numbers in a list, set, or tuple -func Sum(list cty.Value) (cty.Value, error) { - return SumFunc.Call([]cty.Value{list}) -} - -// Transpose takes a map of lists of strings and swaps the keys and values to -// produce a new map of lists of strings. -func Transpose(values cty.Value) (cty.Value, error) { - return TransposeFunc.Call([]cty.Value{values}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/conversion.go b/pkg/iac/scanners/terraform/parser/funcs/conversion.go index 02fb3164a6f0..18a1310e69f7 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/conversion.go +++ b/pkg/iac/scanners/terraform/parser/funcs/conversion.go @@ -2,10 +2,7 @@ package funcs import ( - "fmt" - "sort" "strconv" - "strings" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" @@ -32,9 +29,10 @@ func MakeToFunc(wantTy cty.Type) function.Function { // messages to be more appropriate for an explicit type // conversion, whereas the cty function system produces // messages aimed at _implicit_ type conversions. - Type: cty.DynamicPseudoType, - AllowNull: true, - AllowMarked: true, + Type: cty.DynamicPseudoType, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, }, }, Type: func(args []cty.Value) (cty.Type, error) { @@ -96,128 +94,3 @@ func MakeToFunc(wantTy cty.Type) function.Function { }, }) } - -var TypeFunc = function.New(&function.Spec{ - Params: []function.Parameter{ - { - Name: "value", - Type: cty.DynamicPseudoType, - AllowDynamicType: true, - AllowUnknown: true, - AllowNull: true, - }, - }, - Type: function.StaticReturnType(cty.String), - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - return cty.StringVal(TypeString(args[0].Type())).Mark(MarkedRaw), nil - }, -}) - -// Modified copy of TypeString from go-cty: -// https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go -// -// TypeString returns a string representation of a given type that is -// reminiscent of Go syntax calling into the cty package but is mainly -// intended for easy human inspection of values in tests, debug output, etc. -// -// The resulting string will include newlines and indentation in order to -// increase the readability of complex structures. It always ends with a -// newline, so you can print this result directly to your output. -func TypeString(ty cty.Type) string { - var b strings.Builder - writeType(ty, &b, 0) - return b.String() -} - -func writeType(ty cty.Type, b *strings.Builder, indent int) { - switch { - case ty == cty.NilType: - b.WriteString("nil") - return - case ty.IsObjectType(): - atys := ty.AttributeTypes() - if len(atys) == 0 { - b.WriteString("object({})") - return - } - attrNames := make([]string, 0, len(atys)) - for name := range atys { - attrNames = append(attrNames, name) - } - sort.Strings(attrNames) - b.WriteString("object({\n") - indent++ - for _, name := range attrNames { - aty := atys[name] - b.WriteString(indentSpaces(indent)) - fmt.Fprintf(b, "%s: ", name) - writeType(aty, b, indent) - b.WriteString(",\n") - } - indent-- - b.WriteString(indentSpaces(indent)) - b.WriteString("})") - case ty.IsTupleType(): - etys := ty.TupleElementTypes() - if len(etys) == 0 { - b.WriteString("tuple([])") - return - } - b.WriteString("tuple([\n") - indent++ - for _, ety := range etys { - b.WriteString(indentSpaces(indent)) - writeType(ety, b, indent) - b.WriteString(",\n") - } - indent-- - b.WriteString(indentSpaces(indent)) - b.WriteString("])") - case ty.IsCollectionType(): - ety := ty.ElementType() - switch { - case ty.IsListType(): - b.WriteString("list(") - case ty.IsMapType(): - b.WriteString("map(") - case ty.IsSetType(): - b.WriteString("set(") - default: - // At the time of writing there are no other collection types, - // but we'll be robust here and just pass through the GoString - // of anything we don't recognize. - b.WriteString(ty.FriendlyName()) - return - } - // Because object and tuple types render split over multiple - // lines, a collection type container around them can end up - // being hard to see when scanning, so we'll generate some extra - // indentation to make a collection of structural type more visually - // distinct from the structural type alone. - complexElem := ety.IsObjectType() || ety.IsTupleType() - if complexElem { - indent++ - b.WriteString("\n") - b.WriteString(indentSpaces(indent)) - } - writeType(ty.ElementType(), b, indent) - if complexElem { - indent-- - b.WriteString(",\n") - b.WriteString(indentSpaces(indent)) - } - b.WriteString(")") - default: - // For any other type we'll just use its GoString and assume it'll - // follow the usual GoString conventions. - b.WriteString(ty.FriendlyName()) - } -} - -func indentSpaces(level int) string { - return strings.Repeat(" ", level) -} - -func Type(input []cty.Value) (cty.Value, error) { - return TypeFunc.Call(input) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/crypto.go b/pkg/iac/scanners/terraform/parser/funcs/crypto.go index 4bc3bab99744..c2c52842cf7f 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/crypto.go +++ b/pkg/iac/scanners/terraform/parser/funcs/crypto.go @@ -26,8 +26,9 @@ import ( ) var UUIDFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - Type: function.StaticReturnType(cty.String), + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result, err := uuid.GenerateUUID() if err != nil { @@ -48,7 +49,8 @@ var UUIDV5Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var namespace uuidv5.UUID switch { @@ -102,7 +104,8 @@ var BcryptFunc = function.New(&function.Spec{ Name: "cost", Type: cty.Number, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { defaultCost := 10 @@ -121,7 +124,7 @@ var BcryptFunc = function.New(&function.Spec{ input := args[0].AsString() out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost) if err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("error occurred generating password %s", err.Error()) + return cty.UnknownVal(cty.String), fmt.Errorf("error occured generating password %s", err.Error()) } return cty.StringVal(string(out)), nil @@ -149,7 +152,8 @@ var RsaDecryptFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { s := args[0].AsString() key := args[1].AsString() @@ -166,7 +170,7 @@ var RsaDecryptFunc = function.New(&function.Spec{ case asn1.SyntaxError: errStr = strings.ReplaceAll(e.Error(), "asn1: syntax error", "invalid ASN1 data in the given private key") case asn1.StructuralError: - errStr = strings.ReplaceAll(e.Error(), "asn1: structure error", "invalid ASN1 data in the given private key") + errStr = strings.ReplaceAll(e.Error(), "asn1: struture error", "invalid ASN1 data in the given private key") default: errStr = fmt.Sprintf("invalid private key: %s", e) } @@ -186,7 +190,7 @@ var RsaDecryptFunc = function.New(&function.Spec{ }, }) -// Sha1Func constructs a function that computes the SHA1 hash of a given string +// Sha1Func contructs a function that computes the SHA1 hash of a given string // and encodes it with hexadecimal digits. var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString) @@ -196,7 +200,7 @@ func MakeFileSha1Func(target fs.FS, baseDir string) function.Function { return makeFileHashFunction(target, baseDir, sha1.New, hex.EncodeToString) } -// Sha256Func constructs a function that computes the SHA256 hash of a given string +// Sha256Func contructs a function that computes the SHA256 hash of a given string // and encodes it with hexadecimal digits. var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString) @@ -206,7 +210,7 @@ func MakeFileSha256Func(target fs.FS, baseDir string) function.Function { return makeFileHashFunction(target, baseDir, sha256.New, hex.EncodeToString) } -// Sha512Func constructs a function that computes the SHA512 hash of a given string +// Sha512Func contructs a function that computes the SHA512 hash of a given string // and encodes it with hexadecimal digits. var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString) @@ -224,7 +228,8 @@ func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) functi Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { s := args[0].AsString() h := hf() @@ -243,13 +248,15 @@ func makeFileHashFunction(target fs.FS, baseDir string, hf func() hash.Hash, enc Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { path := args[0].AsString() f, err := openFile(target, baseDir, path) if err != nil { return cty.UnknownVal(cty.String), err } + defer f.Close() h := hf() _, err = io.Copy(h, f) @@ -261,75 +268,3 @@ func makeFileHashFunction(target fs.FS, baseDir string, hf func() hash.Hash, enc }, }) } - -// UUID generates and returns a Type-4 UUID in the standard hexadecimal string -// format. -// -// This is not a pure function: it will generate a different result for each -// call. It must therefore be registered as an impure function in the function -// table in the "lang" package. -func UUID() (cty.Value, error) { - return UUIDFunc.Call(nil) -} - -// UUIDV5 generates and returns a Type-5 UUID in the standard hexadecimal string -// format. -func UUIDV5(namespace, name cty.Value) (cty.Value, error) { - return UUIDV5Func.Call([]cty.Value{namespace, name}) -} - -// Base64Sha256 computes the SHA256 hash of a given string and encodes it with -// Base64. -// -// The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied -// as defined in RFC 4634. The raw hash is then encoded with Base64 before returning. -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -func Base64Sha256(str cty.Value) (cty.Value, error) { - return Base64Sha256Func.Call([]cty.Value{str}) -} - -// Base64Sha512 computes the SHA512 hash of a given string and encodes it with -// Base64. -// -// The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied -// as defined in RFC 4634. The raw hash is then encoded with Base64 before returning. -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4 -func Base64Sha512(str cty.Value) (cty.Value, error) { - return Base64Sha512Func.Call([]cty.Value{str}) -} - -// Bcrypt computes a hash of the given string using the Blowfish cipher, -// returning a string in the Modular Crypt Format -// usually expected in the shadow password file on many Unix systems. -func Bcrypt(str cty.Value, cost ...cty.Value) (cty.Value, error) { - args := make([]cty.Value, len(cost)+1) - args[0] = str - copy(args[1:], cost) - return BcryptFunc.Call(args) -} - -// Md5 computes the MD5 hash of a given string and encodes it with hexadecimal digits. -func Md5(str cty.Value) (cty.Value, error) { - return Md5Func.Call([]cty.Value{str}) -} - -// RsaDecrypt decrypts an RSA-encrypted ciphertext, returning the corresponding -// cleartext. -func RsaDecrypt(ciphertext, privatekey cty.Value) (cty.Value, error) { - return RsaDecryptFunc.Call([]cty.Value{ciphertext, privatekey}) -} - -// Sha1 computes the SHA1 hash of a given string and encodes it with hexadecimal digits. -func Sha1(str cty.Value) (cty.Value, error) { - return Sha1Func.Call([]cty.Value{str}) -} - -// Sha256 computes the SHA256 hash of a given string and encodes it with hexadecimal digits. -func Sha256(str cty.Value) (cty.Value, error) { - return Sha256Func.Call([]cty.Value{str}) -} - -// Sha512 computes the SHA512 hash of a given string and encodes it with hexadecimal digits. -func Sha512(str cty.Value) (cty.Value, error) { - return Sha512Func.Call([]cty.Value{str}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/datetime.go b/pkg/iac/scanners/terraform/parser/funcs/datetime.go index b09da879da99..11ed3c8a2214 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/datetime.go +++ b/pkg/iac/scanners/terraform/parser/funcs/datetime.go @@ -2,6 +2,7 @@ package funcs import ( + "fmt" "time" "github.com/zclconf/go-cty/cty" @@ -10,13 +11,26 @@ import ( // TimestampFunc constructs a function that returns a string representation of the current date and time. var TimestampFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - Type: function.StaticReturnType(cty.String), + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil }, }) +// MakeStaticTimestampFunc constructs a function that returns a string +// representation of the date and time specified by the provided argument. +func MakeStaticTimestampFunc(static time.Time) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(static.Format(time.RFC3339)), nil + }, + }) +} + // TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. var TimeAddFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -29,9 +43,10 @@ var TimeAddFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - ts, err := time.Parse(time.RFC3339, args[0].AsString()) + ts, err := parseTimestamp(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), err } @@ -44,28 +59,99 @@ var TimeAddFunc = function.New(&function.Spec{ }, }) -// Timestamp returns a string representation of the current date and time. -// -// In the Terraform language, timestamps are conventionally represented as -// strings using RFC 3339 "Date and Time format" syntax, and so timestamp -// returns a string in this format. -func Timestamp() (cty.Value, error) { - return TimestampFunc.Call([]cty.Value{}) -} +// TimeCmpFunc is a function that compares two timestamps. +var TimeCmpFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "timestamp_a", + Type: cty.String, + }, + { + Name: "timestamp_b", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + tsA, err := parseTimestamp(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(0, err) + } + tsB, err := parseTimestamp(args[1].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(1, err) + } -// TimeAdd adds a duration to a timestamp, returning a new timestamp. -// -// In the Terraform language, timestamps are conventionally represented as -// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires -// the timestamp argument to be a string conforming to this syntax. -// -// `duration` is a string representation of a time difference, consisting of -// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted -// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first -// number may be negative to indicate a negative duration, like `"-2h5m"`. -// -// The result is a string, also in RFC 3339 format, representing the result -// of adding the given direction to the given timestamp. -func TimeAdd(timestamp, duration cty.Value) (cty.Value, error) { - return TimeAddFunc.Call([]cty.Value{timestamp, duration}) + switch { + case tsA.Equal(tsB): + return cty.NumberIntVal(0), nil + case tsA.Before(tsB): + return cty.NumberIntVal(-1), nil + default: + // By elimintation, tsA must be after tsB. + return cty.NumberIntVal(1), nil + } + }, +}) + +func parseTimestamp(ts string) (time.Time, error) { + t, err := time.Parse(time.RFC3339, ts) + if err != nil { + switch err := err.(type) { + case *time.ParseError: + // If err is a time.ParseError then its string representation is not + // appropriate since it relies on details of Go's strange date format + // representation, which a caller of our functions is not expected + // to be familiar with. + // + // Therefore we do some light transformation to get a more suitable + // error that should make more sense to our callers. These are + // still not awesome error messages, but at least they refer to + // the timestamp portions by name rather than by Go's example + // values. + if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { + // For some reason err.Message is populated with a ": " prefix + // by the time package. + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) + } + var what string + switch err.LayoutElem { + case "2006": + what = "year" + case "01": + what = "month" + case "02": + what = "day of month" + case "15": + what = "hour" + case "04": + what = "minute" + case "05": + what = "second" + case "Z07:00": + what = "UTC offset" + case "T": + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'") + case ":", "-": + if err.ValueElem == "" { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) + } else { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) + } + default: + // Should never get here, because time.RFC3339 includes only the + // above portions, but since that might change in future we'll + // be robust here. + what = "timestamp segment" + } + if err.ValueElem == "" { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) + } else { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) + } + } + return time.Time{}, err + } + return t, nil } diff --git a/pkg/iac/scanners/terraform/parser/funcs/defaults.go b/pkg/iac/scanners/terraform/parser/funcs/defaults.go deleted file mode 100644 index 1e5c0913adbd..000000000000 --- a/pkg/iac/scanners/terraform/parser/funcs/defaults.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copied from github.com/hashicorp/terraform/internal/lang/funcs -package funcs - -import ( - "fmt" - - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/function" -) - -// DefaultsFunc is a helper function for substituting default values in -// place of null values in a given data structure. -// -// See the documentation for function Defaults for more information. -var DefaultsFunc = function.New(&function.Spec{ - Params: []function.Parameter{ - { - Name: "input", - Type: cty.DynamicPseudoType, - AllowNull: true, - AllowMarked: true, - }, - { - Name: "defaults", - Type: cty.DynamicPseudoType, - AllowMarked: true, - }, - }, - Type: func(args []cty.Value) (cty.Type, error) { - // The result type is guaranteed to be the same as the input type, - // since all we're doing is replacing null values with non-null - // values of the same type. - retType := args[0].Type() - defaultsType := args[1].Type() - - // This function is aimed at filling in object types or collections - // of object types where some of the attributes might be null, so - // it doesn't make sense to use a primitive type directly with it. - // (The "coalesce" function may be appropriate for such cases.) - if retType.IsPrimitiveType() { - // This error message is a bit of a fib because we can actually - // apply defaults to tuples too, but we expect that to be so - // unusual as to not be worth mentioning here, because mentioning - // it would require using some less-well-known Terraform language - // terminology in the message (tuple types, structural types). - return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied") - } - - defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate - if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil { - errMsg := err.Error() - return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg) - } - - return retType, nil - }, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - if args[0].Type().HasDynamicTypes() { - // If the types our input object aren't known yet for some reason - // then we'll defer all of our work here, because our - // interpretation of the defaults depends on the types in - // the input. - return cty.UnknownVal(retType), nil - } - - v := defaultsApply(args[0], args[1]) - return v, nil - }, -}) - -// nolint: gocyclo -func defaultsApply(input, fallback cty.Value) cty.Value { - wantTy := input.Type() - - umInput, inputMarks := input.Unmark() - umFb, fallbackMarks := fallback.Unmark() - - // If neither are known, we very conservatively return an unknown value - // with the union of marks on both input and default. - if !(umInput.IsKnown() && umFb.IsKnown()) { - return cty.UnknownVal(wantTy).WithMarks(inputMarks).WithMarks(fallbackMarks) - } - - // For the rest of this function we're assuming that the given defaults - // will always be valid, because we expect to have caught any problems - // during the type checking phase. Any inconsistencies that reach here are - // therefore considered to be implementation bugs, and so will panic. - - // Our strategy depends on the kind of type we're working with. - switch { - case wantTy.IsPrimitiveType(): - // For leaf primitive values the rule is relatively simple: use the - // input if it's non-null, or fallback if input is null. - if !umInput.IsNull() { - return input - } - v, err := convert.Convert(umFb, wantTy) - if err != nil { - // Should not happen because we checked in defaultsAssertSuitableFallback - panic(err.Error()) - } - return v.WithMarks(fallbackMarks) - - case wantTy.IsObjectType(): - // For structural types, a null input value must be passed through. We - // do not apply default values for missing optional structural values, - // only their contents. - // - // We also pass through the input if the fallback value is null. This - // can happen if the given defaults do not include a value for this - // attribute. - if umInput.IsNull() || umFb.IsNull() { - return input - } - atys := wantTy.AttributeTypes() - ret := make(map[string]cty.Value) - for attr, aty := range atys { - inputSub := umInput.GetAttr(attr) - fallbackSub := cty.NullVal(aty) - if umFb.Type().HasAttribute(attr) { - fallbackSub = umFb.GetAttr(attr) - } - ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks)) - } - return cty.ObjectVal(ret) - - case wantTy.IsTupleType(): - // For structural types, a null input value must be passed through. We - // do not apply default values for missing optional structural values, - // only their contents. - // - // We also pass through the input if the fallback value is null. This - // can happen if the given defaults do not include a value for this - // attribute. - if umInput.IsNull() || umFb.IsNull() { - return input - } - - l := wantTy.Length() - ret := make([]cty.Value, l) - for i := 0; i < l; i++ { - inputSub := umInput.Index(cty.NumberIntVal(int64(i))) - fallbackSub := umFb.Index(cty.NumberIntVal(int64(i))) - ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks)) - } - return cty.TupleVal(ret) - - case wantTy.IsCollectionType(): - // For collection types we apply a single fallback value to each - // element of the input collection, because in the situations this - // function is intended for we assume that the number of elements - // is the caller's decision, and so we'll just apply the same defaults - // to all of the elements. - ety := wantTy.ElementType() - switch { - case wantTy.IsMapType(): - newVals := make(map[string]cty.Value) - - if !umInput.IsNull() { - for it := umInput.ElementIterator(); it.Next(); { - k, v := it.Element() - newVals[k.AsString()] = defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks)) - } - } - - if len(newVals) == 0 { - return cty.MapValEmpty(ety) - } - return cty.MapVal(newVals) - case wantTy.IsListType(), wantTy.IsSetType(): - var newVals []cty.Value - - if !umInput.IsNull() { - for it := umInput.ElementIterator(); it.Next(); { - _, v := it.Element() - newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks)) - newVals = append(newVals, newV) - } - } - - if len(newVals) == 0 { - if wantTy.IsSetType() { - return cty.SetValEmpty(ety) - } - return cty.ListValEmpty(ety) - } - if wantTy.IsSetType() { - return cty.SetVal(newVals) - } - return cty.ListVal(newVals) - default: - // There are no other collection types, so this should not happen - panic(fmt.Sprintf("invalid collection type %#v", wantTy)) - } - default: - // We should've caught anything else in defaultsAssertSuitableFallback, - // so this should not happen. - panic(fmt.Sprintf("invalid target type %#v", wantTy)) - } -} - -func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error { - // If the type we want is a collection type then we need to keep peeling - // away collection type wrappers until we find the non-collection-type - // that's underneath, which is what the fallback will actually be applied - // to. - inCollection := false - for wantTy.IsCollectionType() { - wantTy = wantTy.ElementType() - inCollection = true - } - - switch { - case wantTy.IsPrimitiveType(): - // The fallback is valid if it's equal to or convertible to what we want. - if fallbackTy.Equals(wantTy) { - return nil - } - conversion := convert.GetConversion(fallbackTy, wantTy) - if conversion == nil { - msg := convert.MismatchMessage(fallbackTy, wantTy) - return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg) - } - return nil - case wantTy.IsObjectType(): - if !fallbackTy.IsObjectType() { - if inCollection { - return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName()) - } - return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName()) - } - for attr, wantAty := range wantTy.AttributeTypes() { - if !fallbackTy.HasAttribute(attr) { - continue // it's always okay to not have a default value - } - fallbackSubpath := fallbackPath.GetAttr(attr) - fallbackSubTy := fallbackTy.AttributeType(attr) - err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath) - if err != nil { - return err - } - } - for attr := range fallbackTy.AttributeTypes() { - if !wantTy.HasAttribute(attr) { - fallbackSubpath := fallbackPath.GetAttr(attr) - return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr) - } - } - return nil - case wantTy.IsTupleType(): - if !fallbackTy.IsTupleType() { - if inCollection { - return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName()) - } - return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName()) - } - wantEtys := wantTy.TupleElementTypes() - fallbackEtys := fallbackTy.TupleElementTypes() - if got, want := len(wantEtys), len(fallbackEtys); got != want { - return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got) - } - for i := 0; i < len(wantEtys); i++ { - fallbackSubpath := fallbackPath.IndexInt(i) - wantSubTy := wantEtys[i] - fallbackSubTy := fallbackEtys[i] - err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath) - if err != nil { - return err - } - } - return nil - default: - // No other types are supported right now. - return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName()) - } -} - -// Defaults is a helper function for substituting default values in -// place of null values in a given data structure. -// -// This is primarily intended for use with a module input variable that -// has an object type constraint (or a collection thereof) that has optional -// attributes, so that the receiver of a value that omits those attributes -// can insert non-null default values in place of the null values caused by -// omitting the attributes. -func Defaults(input, defaults cty.Value) (cty.Value, error) { - return DefaultsFunc.Call([]cty.Value{input, defaults}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/encoding.go b/pkg/iac/scanners/terraform/parser/funcs/encoding.go index 778367fb8fce..e5fb8490818f 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/encoding.go +++ b/pkg/iac/scanners/terraform/parser/funcs/encoding.go @@ -19,22 +19,25 @@ import ( var Base64DecodeFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "str", - Type: cty.String, + Name: "str", + Type: cty.String, + AllowMarked: true, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - s := args[0].AsString() + str, strMarks := args[0].Unmark() + s := str.AsString() sDec, err := base64.StdEncoding.DecodeString(s) if err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks)) } - if !utf8.Valid(sDec) { - log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", sDec) + if !utf8.Valid([]byte(sDec)) { + log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", redactIfSensitive(sDec, strMarks)) return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8") } - return cty.StringVal(string(sDec)), nil + return cty.StringVal(string(sDec)).WithMarks(strMarks), nil }, }) @@ -46,7 +49,8 @@ var Base64EncodeFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil }, @@ -64,7 +68,8 @@ var TextEncodeBase64Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) if err != nil || encoding == nil { @@ -107,7 +112,8 @@ var TextDecodeBase64Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) if err != nil || encoding == nil { @@ -126,7 +132,7 @@ var TextDecodeBase64Func = function.New(&function.Spec{ case base64.CorruptInputError: return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err)) default: - return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %T", err) + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err) } } @@ -150,20 +156,21 @@ var Base64GzipFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { s := args[0].AsString() var b bytes.Buffer gz := gzip.NewWriter(&b) if _, err := gz.Write([]byte(s)); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: %w", err) } if err := gz.Flush(); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: %w", err) } if err := gz.Close(); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: %w", err) } return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil }, @@ -177,78 +184,9 @@ var URLEncodeFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(url.QueryEscape(args[0].AsString())), nil }, }) - -// Base64Decode decodes a string containing a base64 sequence. -// -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -// -// Strings in the Terraform language are sequences of unicode characters rather -// than bytes, so this function will also interpret the resulting bytes as -// UTF-8. If the bytes after Base64 decoding are _not_ valid UTF-8, this function -// produces an error. -func Base64Decode(str cty.Value) (cty.Value, error) { - return Base64DecodeFunc.Call([]cty.Value{str}) -} - -// Base64Encode applies Base64 encoding to a string. -// -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -// -// Strings in the Terraform language are sequences of unicode characters rather -// than bytes, so this function will first encode the characters from the string -// as UTF-8, and then apply Base64 encoding to the result. -func Base64Encode(str cty.Value) (cty.Value, error) { - return Base64EncodeFunc.Call([]cty.Value{str}) -} - -// Base64Gzip compresses a string with gzip and then encodes the result in -// Base64 encoding. -// -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -// -// Strings in the Terraform language are sequences of unicode characters rather -// than bytes, so this function will first encode the characters from the string -// as UTF-8, then apply gzip compression, and then finally apply Base64 encoding. -func Base64Gzip(str cty.Value) (cty.Value, error) { - return Base64GzipFunc.Call([]cty.Value{str}) -} - -// URLEncode applies URL encoding to a given string. -// -// This function identifies characters in the given string that would have a -// special meaning when included as a query string argument in a URL and -// escapes them using RFC 3986 "percent encoding". -// -// If the given string contains non-ASCII characters, these are first encoded as -// UTF-8 and then percent encoding is applied separately to each UTF-8 byte. -func URLEncode(str cty.Value) (cty.Value, error) { - return URLEncodeFunc.Call([]cty.Value{str}) -} - -// TextEncodeBase64 applies Base64 encoding to a string that was encoded before with a target encoding. -// -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -// -// First step is to apply the target IANA encoding (e.g. UTF-16LE). -// Strings in the Terraform language are sequences of unicode characters rather -// than bytes, so this function will first encode the characters from the string -// as UTF-8, and then apply Base64 encoding to the result. -func TextEncodeBase64(str, enc cty.Value) (cty.Value, error) { - return TextEncodeBase64Func.Call([]cty.Value{str, enc}) -} - -// TextDecodeBase64 decodes a string containing a base64 sequence whereas a specific encoding of the string is expected. -// -// Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4. -// -// Strings in the Terraform language are sequences of unicode characters rather -// than bytes, so this function will also interpret the resulting bytes as -// the target encoding. -func TextDecodeBase64(str, enc cty.Value) (cty.Value, error) { - return TextDecodeBase64Func.Call([]cty.Value{str, enc}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/filesystem.go b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go index 910e17f325c6..64e1c8b0ed19 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/filesystem.go +++ b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go @@ -3,6 +3,7 @@ package funcs import ( "encoding/base64" + "errors" "fmt" "io" "io/fs" @@ -184,7 +185,7 @@ func MakeTemplateFileFunc(target fs.FS, baseDir string, funcsCb func() map[strin // MakeFileExistsFunc constructs a function that takes a path // and determines whether a file exists at that path -func MakeFileExistsFunc(baseDir string) function.Function { +func MakeFileExistsFunc(target fs.FS, baseDir string) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -204,12 +205,12 @@ func MakeFileExistsFunc(baseDir string) function.Function { path = filepath.Join(baseDir, path) } - // Ensure that the path is canonical for the host OS - path = filepath.Clean(path) + // Trivy uses a vitrual file system + path = filepath.ToSlash(path) - fi, err := os.Stat(path) + fi, err := fs.Stat(target, path) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { return cty.False, nil } return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path) @@ -227,7 +228,7 @@ func MakeFileExistsFunc(baseDir string) function.Function { // MakeFileSetFunc constructs a function that takes a glob pattern // and enumerates a file set from that pattern -func MakeFileSetFunc(baseDir string) function.Function { +func MakeFileSetFunc(target fs.FS, baseDir string) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -252,8 +253,10 @@ func MakeFileSetFunc(baseDir string) function.Function { // pattern is canonical for the host OS. The joined path is // automatically cleaned during this operation. pattern = filepath.Join(path, pattern) + // Trivy uses a vitrual file system + path = filepath.ToSlash(path) - matches, err := doublestar.Glob(os.DirFS(path), pattern) + matches, err := doublestar.Glob(target, pattern) if err != nil { return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err) } @@ -364,8 +367,8 @@ func openFile(target fs.FS, baseDir, path string) (fs.File, error) { path = filepath.Join(baseDir, path) } - // Ensure that the path is canonical for the host OS - path = filepath.Clean(path) + // Trivy uses a vitrual file system + path = filepath.ToSlash(path) if target != nil { return target.Open(path) @@ -402,66 +405,3 @@ func File(target fs.FS, baseDir string, path cty.Value) (cty.Value, error) { fn := MakeFileFunc(target, baseDir, false) return fn.Call([]cty.Value{path}) } - -// FileExists determines whether a file exists at the given path. -// -// The underlying function implementation works relative to a particular base -// directory, so this wrapper takes a base directory string and uses it to -// construct the underlying function before calling it. -func FileExists(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileExistsFunc(baseDir) - return fn.Call([]cty.Value{path}) -} - -// FileSet enumerates a set of files given a glob pattern -// -// The underlying function implementation works relative to a particular base -// directory, so this wrapper takes a base directory string and uses it to -// construct the underlying function before calling it. -func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) { - fn := MakeFileSetFunc(baseDir) - return fn.Call([]cty.Value{path, pattern}) -} - -// FileBase64 reads the contents of the file at the given path. -// -// The bytes from the file are encoded as base64 before returning. -// -// The underlying function implementation works relative to a particular base -// directory, so this wrapper takes a base directory string and uses it to -// construct the underlying function before calling it. -func FileBase64(target fs.FS, baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileFunc(target, baseDir, true) - return fn.Call([]cty.Value{path}) -} - -// Basename takes a string containing a filesystem path and removes all except the last portion from it. -// -// The underlying function implementation works only with the path string and does not access the filesystem itself. -// It is therefore unable to take into account filesystem features such as symlinks. -// -// If the path is empty then the result is ".", representing the current working directory. -func Basename(path cty.Value) (cty.Value, error) { - return BasenameFunc.Call([]cty.Value{path}) -} - -// Dirname takes a string containing a filesystem path and removes the last portion from it. -// -// The underlying function implementation works only with the path string and does not access the filesystem itself. -// It is therefore unable to take into account filesystem features such as symlinks. -// -// If the path is empty then the result is ".", representing the current working directory. -func Dirname(path cty.Value) (cty.Value, error) { - return DirnameFunc.Call([]cty.Value{path}) -} - -// Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with -// the current user's home directory path. -// -// The underlying function implementation works only with the path string and does not access the filesystem itself. -// It is therefore unable to take into account filesystem features such as symlinks. -// -// If the leading segment in the path is not `~` then the given path is returned unmodified. -func Pathexpand(path cty.Value) (cty.Value, error) { - return PathExpandFunc.Call([]cty.Value{path}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/ip.go b/pkg/iac/scanners/terraform/parser/funcs/ip.go new file mode 100644 index 000000000000..42bcfba4e618 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/ip.go @@ -0,0 +1,261 @@ +// Copied from github.com/hashicorp/terraform/internal/ipaddr +package funcs + +import ( + stdnet "net" +) + +// Bigger than we need, not too big to worry about overflow +const bigVal = 0xFFFFFF + +// Decimal to integer. +// Returns number, characters consumed, success. +func dtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') + if n >= bigVal { + return bigVal, i, false + } + } + if i == 0 { + return 0, 0, false + } + return n, i, true +} + +// Hexadecimal to integer. +// Returns number, characters consumed, success. +func xtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' { + n *= 16 + n += int(s[i] - '0') + } else if 'a' <= s[i] && s[i] <= 'f' { + n *= 16 + n += int(s[i]-'a') + 10 + } else if 'A' <= s[i] && s[i] <= 'F' { + n *= 16 + n += int(s[i]-'A') + 10 + } else { + break + } + if n >= bigVal { + return 0, i, false + } + } + if i == 0 { + return 0, i, false + } + return n, i, true +} + +// +// Lean on the standard net lib as much as possible. +// + +type IP = stdnet.IP +type IPNet = stdnet.IPNet +type ParseError = stdnet.ParseError + +const IPv4len = stdnet.IPv4len +const IPv6len = stdnet.IPv6len + +var CIDRMask = stdnet.CIDRMask +var IPv4 = stdnet.IPv4 + +// Parse IPv4 address (d.d.d.d). +func parseIPv4(s string) IP { + var p [IPv4len]byte + for i := 0; i < IPv4len; i++ { + if len(s) == 0 { + // Missing octets. + return nil + } + if i > 0 { + if s[0] != '.' { + return nil + } + s = s[1:] + } + n, c, ok := dtoi(s) + if !ok || n > 0xFF { + return nil + } + // + // NOTE: This correct check was added for go-1.17, but is a + // backwards-incompatible change for Terraform users, who might have + // already written modules with leading zeroes. + // + //if c > 1 && s[0] == '0' { + // // Reject non-zero components with leading zeroes. + // return nil + //} + s = s[c:] + p[i] = byte(n) + } + if len(s) != 0 { + return nil + } + return IPv4(p[0], p[1], p[2], p[3]) +} + +// parseIPv6 parses s as a literal IPv6 address described in RFC 4291 +// and RFC 5952. +func parseIPv6(s string) (ip IP) { + ip = make(IP, IPv6len) + ellipsis := -1 // position of ellipsis in ip + + // Might have leading ellipsis + if len(s) >= 2 && s[0] == ':' && s[1] == ':' { + ellipsis = 0 + s = s[2:] + // Might be only ellipsis + if len(s) == 0 { + return ip + } + } + + // Loop, parsing hex numbers followed by colon. + i := 0 + for i < IPv6len { + // Hex number. + n, c, ok := xtoi(s) + if !ok || n > 0xFFFF { + return nil + } + + // If followed by dot, might be in trailing IPv4. + if c < len(s) && s[c] == '.' { + if ellipsis < 0 && i != IPv6len-IPv4len { + // Not the right place. + return nil + } + if i+IPv4len > IPv6len { + // Not enough room. + return nil + } + ip4 := parseIPv4(s) + if ip4 == nil { + return nil + } + ip[i] = ip4[12] + ip[i+1] = ip4[13] + ip[i+2] = ip4[14] + ip[i+3] = ip4[15] + s = "" + i += IPv4len + break + } + + // Save this 16-bit chunk. + ip[i] = byte(n >> 8) + ip[i+1] = byte(n) + i += 2 + + // Stop at end of string. + s = s[c:] + if len(s) == 0 { + break + } + + // Otherwise must be followed by colon and more. + if s[0] != ':' || len(s) == 1 { + return nil + } + s = s[1:] + + // Look for ellipsis. + if s[0] == ':' { + if ellipsis >= 0 { // already have one + return nil + } + ellipsis = i + s = s[1:] + if len(s) == 0 { // can be at end + break + } + } + } + + // Must have used entire string. + if len(s) != 0 { + return nil + } + + // If didn't parse enough, expand ellipsis. + if i < IPv6len { + if ellipsis < 0 { + return nil + } + n := IPv6len - i + for j := i - 1; j >= ellipsis; j-- { + ip[j+n] = ip[j] + } + for j := ellipsis + n - 1; j >= ellipsis; j-- { + ip[j] = 0 + } + } else if ellipsis >= 0 { + // Ellipsis must represent at least one 0 group. + return nil + } + return ip +} + +// ParseIP parses s as an IP address, returning the result. +// The string s can be in IPv4 dotted decimal ("192.0.2.1"), IPv6 +// ("2001:db8::68"), or IPv4-mapped IPv6 ("::ffff:192.0.2.1") form. +// If s is not a valid textual representation of an IP address, +// ParseIP returns nil. +func ParseIP(s string) IP { + for i := 0; i < len(s); i++ { + switch s[i] { + case '.': + return parseIPv4(s) + case ':': + return parseIPv6(s) + } + } + return nil +} + +// ParseCIDR parses s as a CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in +// RFC 4632 and RFC 4291. +// +// It returns the IP address and the network implied by the IP and +// prefix length. +// For example, ParseCIDR("192.0.2.1/24") returns the IP address +// 192.0.2.1 and the network 192.0.2.0/24. +func ParseCIDR(s string) (IP, *IPNet, error) { + i := indexByteString(s, '/') + if i < 0 { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + addr, mask := s[:i], s[i+1:] + iplen := IPv4len + ip := parseIPv4(addr) + if ip == nil { + iplen = IPv6len + ip = parseIPv6(addr) + } + n, i, ok := dtoi(mask) + if ip == nil || !ok || i != len(mask) || n < 0 || n > 8*iplen { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + m := CIDRMask(n, 8*iplen) + return ip, &IPNet{IP: ip.Mask(m), Mask: m}, nil +} + +// This is copied from go/src/internal/bytealg, which includes versions +// optimized for various platforms. Those optimizations are elided here so we +// don't have to maintain them. +func indexByteString(s string, c byte) int { + for i := 0; i < len(s); i++ { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/marks.go b/pkg/iac/scanners/terraform/parser/funcs/marks.go index ca368c113c5c..abbc397f1e08 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/marks.go +++ b/pkg/iac/scanners/terraform/parser/funcs/marks.go @@ -37,8 +37,8 @@ func Contains(val cty.Value, mark valueMark) bool { // MarkedSensitive indicates that this value is marked as sensitive in the context of // Terraform. -var MarkedSensitive = valueMark("sensitive") +const MarkedSensitive = valueMark("sensitive") // MarkedRaw is used to indicate to the repl that the value should be written without // any formatting. -var MarkedRaw = valueMark("raw") +const MarkedRaw = valueMark("raw") diff --git a/pkg/iac/scanners/terraform/parser/funcs/number.go b/pkg/iac/scanners/terraform/parser/funcs/number.go index 012455eb7737..3fd606792177 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/number.go +++ b/pkg/iac/scanners/terraform/parser/funcs/number.go @@ -10,7 +10,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -// LogFunc constructs a function that returns the logarithm of a given number in a given base. +// LogFunc contructs a function that returns the logarithm of a given number in a given base. var LogFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -22,7 +22,8 @@ var LogFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num float64 if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -38,7 +39,7 @@ var LogFunc = function.New(&function.Spec{ }, }) -// PowFunc constructs a function that returns the logarithm of a given number in a given base. +// PowFunc contructs a function that returns the logarithm of a given number in a given base. var PowFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -50,7 +51,8 @@ var PowFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num float64 if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -66,7 +68,7 @@ var PowFunc = function.New(&function.Spec{ }, }) -// SignumFunc constructs a function that returns the closest whole number greater +// SignumFunc contructs a function that returns the closest whole number greater // than or equal to the given value. var SignumFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -75,7 +77,8 @@ var SignumFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num int if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -92,16 +95,18 @@ var SignumFunc = function.New(&function.Spec{ }, }) -// ParseIntFunc constructs a function that parses a string argument and returns an integer of the specified base. +// ParseIntFunc contructs a function that parses a string argument and returns an integer of the specified base. var ParseIntFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "number", - Type: cty.DynamicPseudoType, + Name: "number", + Type: cty.DynamicPseudoType, + AllowMarked: true, }, { - Name: "base", - Type: cty.Number, + Name: "base", + Type: cty.Number, + AllowMarked: true, }, }, @@ -111,17 +116,20 @@ var ParseIntFunc = function.New(&function.Spec{ } return cty.Number, nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { var numstr string var base int var err error - if err = gocty.FromCtyValue(args[0], &numstr); err != nil { + numArg, numMarks := args[0].Unmark() + if err = gocty.FromCtyValue(numArg, &numstr); err != nil { return cty.UnknownVal(cty.String), function.NewArgError(0, err) } - if err = gocty.FromCtyValue(args[1], &base); err != nil { + baseArg, baseMarks := args[1].Unmark() + if err = gocty.FromCtyValue(baseArg, &base); err != nil { return cty.UnknownVal(cty.Number), function.NewArgError(1, err) } @@ -136,35 +144,14 @@ var ParseIntFunc = function.New(&function.Spec{ if !ok { return cty.UnknownVal(cty.Number), function.NewArgErrorf( 0, - "cannot parse %q as a base %d integer", - numstr, - base, + "cannot parse %s as a base %s integer", + redactIfSensitive(numstr, numMarks), + redactIfSensitive(base, baseMarks), ) } - parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)) + parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)).WithMarks(numMarks, baseMarks) return parsedNum, nil }, }) - -// Log returns returns the logarithm of a given number in a given base. -func Log(num, base cty.Value) (cty.Value, error) { - return LogFunc.Call([]cty.Value{num, base}) -} - -// Pow returns the logarithm of a given number in a given base. -func Pow(num, power cty.Value) (cty.Value, error) { - return PowFunc.Call([]cty.Value{num, power}) -} - -// Signum determines the sign of a number, returning a number between -1 and -// 1 to represent the sign. -func Signum(num cty.Value) (cty.Value, error) { - return SignumFunc.Call([]cty.Value{num}) -} - -// ParseInt parses a string argument and returns an integer of the specified base. -func ParseInt(num, base cty.Value) (cty.Value, error) { - return ParseIntFunc.Call([]cty.Value{num, base}) -} diff --git a/pkg/iac/scanners/terraform/parser/funcs/redact.go b/pkg/iac/scanners/terraform/parser/funcs/redact.go new file mode 100644 index 000000000000..f5908fc7da57 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/redact.go @@ -0,0 +1,20 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" +) + +func redactIfSensitive(value interface{}, markses ...cty.ValueMarks) string { + if Has(cty.DynamicVal.WithMarks(markses...), MarkedSensitive) { + return "(sensitive value)" + } + switch v := value.(type) { + case string: + return fmt.Sprintf("%q", v) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/refinements.go b/pkg/iac/scanners/terraform/parser/funcs/refinements.go new file mode 100644 index 000000000000..de9cb08b1604 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/refinements.go @@ -0,0 +1,10 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "github.com/zclconf/go-cty/cty" +) + +func refineNotNull(b *cty.RefinementBuilder) *cty.RefinementBuilder { + return b.NotNull() +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/sensitive.go b/pkg/iac/scanners/terraform/parser/funcs/sensitive.go index c67ed13e6e7b..3566a678fc9b 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/sensitive.go +++ b/pkg/iac/scanners/terraform/parser/funcs/sensitive.go @@ -49,19 +49,26 @@ var NonsensitiveFunc = function.New(&function.Spec{ return args[0].Type(), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - if args[0].IsKnown() && !args[0].HasMark(MarkedSensitive) { - return cty.DynamicVal, function.NewArgErrorf(0, "the given value is not sensitive, so this call is redundant") - } - v, m := args[0].Unmark() - delete(m, MarkedSensitive) // remove the sensitive marking - return v.WithMarks(m), nil + v, marks := args[0].Unmark() + delete(marks, MarkedSensitive) // remove the sensitive marking + return v.WithMarks(marks), nil }, }) -func Sensitive(v cty.Value) (cty.Value, error) { - return SensitiveFunc.Call([]cty.Value{v}) -} - -func Nonsensitive(v cty.Value) (cty.Value, error) { - return NonsensitiveFunc.Call([]cty.Value{v}) -} +var IssensitiveFunc = function.New(&function.Spec{ + Params: []function.Parameter{{ + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }}, + Type: func(args []cty.Value) (cty.Type, error) { + return cty.Bool, nil + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + s := args[0].HasMark(MarkedSensitive) + return cty.BoolVal(s), nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/string.go b/pkg/iac/scanners/terraform/parser/funcs/string.go index 6fe077c1f586..7c3150047eb6 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/string.go +++ b/pkg/iac/scanners/terraform/parser/funcs/string.go @@ -9,8 +9,85 @@ import ( "github.com/zclconf/go-cty/cty/function" ) +// StartsWithFunc constructs a function that checks if a string starts with +// a specific prefix using strings.HasPrefix +var StartsWithFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + AllowUnknown: true, + }, + { + Name: "prefix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + prefix := args[1].AsString() + + if !args[0].IsKnown() { + // If the unknown value has a known prefix then we might be + // able to still produce a known result. + if prefix == "" { + // The empty string is a prefix of any string. + return cty.True, nil + } + if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" { + if strings.HasPrefix(knownPrefix, prefix) { + return cty.True, nil + } + if len(knownPrefix) >= len(prefix) { + // If the prefix we're testing is no longer than the known + // prefix and it didn't match then the full string with + // that same prefix can't match either. + return cty.False, nil + } + } + return cty.UnknownVal(cty.Bool), nil + } + + str := args[0].AsString() + + if strings.HasPrefix(str, prefix) { + return cty.True, nil + } + + return cty.False, nil + }, +}) + +// EndsWithFunc constructs a function that checks if a string ends with +// a specific suffix using strings.HasSuffix +var EndsWithFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "suffix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + str := args[0].AsString() + suffix := args[1].AsString() + + if strings.HasSuffix(str, suffix) { + return cty.True, nil + } + + return cty.False, nil + }, +}) + // ReplaceFunc constructs a function that searches a given string for another -// given substring, and replaces each occurrence with a given replacement string. +// given substring, and replaces each occurence with a given replacement string. var ReplaceFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -26,7 +103,8 @@ var ReplaceFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { str := args[0].AsString() substr := args[1].AsString() @@ -43,12 +121,32 @@ var ReplaceFunc = function.New(&function.Spec{ return cty.StringVal(re.ReplaceAllString(str, replace)), nil } - return cty.StringVal(strings.ReplaceAll(str, substr, replace)), nil + return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil }, }) -// Replace searches a given string for another given substring, -// and replaces all occurrences with a given replacement string. -func Replace(str, substr, replace cty.Value) (cty.Value, error) { - return ReplaceFunc.Call([]cty.Value{str, substr, replace}) -} +// StrContainsFunc searches a given string for another given substring, +// if found the function returns true, otherwise returns false. +var StrContainsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + + if strings.Contains(str, substr) { + return cty.True, nil + } + + return cty.False, nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/functions.go b/pkg/iac/scanners/terraform/parser/functions.go index 39b6c268b345..6f6406d8ed19 100644 --- a/pkg/iac/scanners/terraform/parser/functions.go +++ b/pkg/iac/scanners/terraform/parser/functions.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" - funcs2 "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/funcs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/funcs" ) // Functions returns the set of functions that should be used to when evaluating @@ -17,104 +17,117 @@ import ( func Functions(target fs.FS, baseDir string) map[string]function.Function { return map[string]function.Function{ "abs": stdlib.AbsoluteFunc, - "abspath": funcs2.AbsPathFunc, - "basename": funcs2.BasenameFunc, - "base64decode": funcs2.Base64DecodeFunc, - "base64encode": funcs2.Base64EncodeFunc, - "base64gzip": funcs2.Base64GzipFunc, - "base64sha256": funcs2.Base64Sha256Func, - "base64sha512": funcs2.Base64Sha512Func, - "bcrypt": funcs2.BcryptFunc, + "abspath": funcs.AbsPathFunc, + "alltrue": funcs.AllTrueFunc, + "anytrue": funcs.AnyTrueFunc, + "basename": funcs.BasenameFunc, + "base64decode": funcs.Base64DecodeFunc, + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, "can": tryfunc.CanFunc, "ceil": stdlib.CeilFunc, "chomp": stdlib.ChompFunc, - "cidrhost": funcs2.CidrHostFunc, - "cidrnetmask": funcs2.CidrNetmaskFunc, - "cidrsubnet": funcs2.CidrSubnetFunc, - "cidrsubnets": funcs2.CidrSubnetsFunc, - "coalesce": funcs2.CoalesceFunc, + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, + "coalesce": funcs.CoalesceFunc, "coalescelist": stdlib.CoalesceListFunc, "compact": stdlib.CompactFunc, "concat": stdlib.ConcatFunc, "contains": stdlib.ContainsFunc, "csvdecode": stdlib.CSVDecodeFunc, - "dirname": funcs2.DirnameFunc, + "dirname": funcs.DirnameFunc, "distinct": stdlib.DistinctFunc, "element": stdlib.ElementFunc, + "endswith": funcs.EndsWithFunc, "chunklist": stdlib.ChunklistFunc, - "file": funcs2.MakeFileFunc(target, baseDir, false), - "fileexists": funcs2.MakeFileExistsFunc(baseDir), - "fileset": funcs2.MakeFileSetFunc(baseDir), - "filebase64": funcs2.MakeFileFunc(target, baseDir, true), - "filebase64sha256": funcs2.MakeFileBase64Sha256Func(target, baseDir), - "filebase64sha512": funcs2.MakeFileBase64Sha512Func(target, baseDir), - "filemd5": funcs2.MakeFileMd5Func(target, baseDir), - "filesha1": funcs2.MakeFileSha1Func(target, baseDir), - "filesha256": funcs2.MakeFileSha256Func(target, baseDir), - "filesha512": funcs2.MakeFileSha512Func(target, baseDir), + "file": funcs.MakeFileFunc(target, baseDir, false), + "fileexists": funcs.MakeFileExistsFunc(target, baseDir), + "fileset": funcs.MakeFileSetFunc(target, baseDir), + "filebase64": funcs.MakeFileFunc(target, baseDir, true), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(target, baseDir), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(target, baseDir), + "filemd5": funcs.MakeFileMd5Func(target, baseDir), + "filesha1": funcs.MakeFileSha1Func(target, baseDir), + "filesha256": funcs.MakeFileSha256Func(target, baseDir), + "filesha512": funcs.MakeFileSha512Func(target, baseDir), "flatten": stdlib.FlattenFunc, "floor": stdlib.FloorFunc, "format": stdlib.FormatFunc, "formatdate": stdlib.FormatDateFunc, "formatlist": stdlib.FormatListFunc, "indent": stdlib.IndentFunc, - "index": funcs2.IndexFunc, // stdlib.IndexFunc is not compatible + "index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible "join": stdlib.JoinFunc, "jsondecode": stdlib.JSONDecodeFunc, "jsonencode": stdlib.JSONEncodeFunc, "keys": stdlib.KeysFunc, - "length": funcs2.LengthFunc, - "list": funcs2.ListFunc, + "length": funcs.LengthFunc, + "list": funcs.ListFunc, "log": stdlib.LogFunc, - "lookup": funcs2.LookupFunc, + "lookup": funcs.LookupFunc, "lower": stdlib.LowerFunc, - "map": funcs2.MapFunc, - "matchkeys": funcs2.MatchkeysFunc, + "map": funcs.MapFunc, + "matchkeys": funcs.MatchkeysFunc, "max": stdlib.MaxFunc, - "md5": funcs2.Md5Func, + "md5": funcs.Md5Func, "merge": stdlib.MergeFunc, "min": stdlib.MinFunc, + "one": funcs.OneFunc, "parseint": stdlib.ParseIntFunc, - "pathexpand": funcs2.PathExpandFunc, + "pathexpand": funcs.PathExpandFunc, "pow": stdlib.PowFunc, "range": stdlib.RangeFunc, "regex": stdlib.RegexFunc, "regexall": stdlib.RegexAllFunc, - "replace": funcs2.ReplaceFunc, + "replace": funcs.ReplaceFunc, "reverse": stdlib.ReverseListFunc, - "rsadecrypt": funcs2.RsaDecryptFunc, + "rsadecrypt": funcs.RsaDecryptFunc, + "sensitive": funcs.SensitiveFunc, + "nonsensitive": funcs.NonsensitiveFunc, + "issensitive": funcs.IssensitiveFunc, "setintersection": stdlib.SetIntersectionFunc, "setproduct": stdlib.SetProductFunc, "setsubtract": stdlib.SetSubtractFunc, "setunion": stdlib.SetUnionFunc, - "sha1": funcs2.Sha1Func, - "sha256": funcs2.Sha256Func, - "sha512": funcs2.Sha512Func, + "sha1": funcs.Sha1Func, + "sha256": funcs.Sha256Func, + "sha512": funcs.Sha512Func, "signum": stdlib.SignumFunc, "slice": stdlib.SliceFunc, "sort": stdlib.SortFunc, "split": stdlib.SplitFunc, + "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, - "timestamp": funcs2.TimestampFunc, + "sum": funcs.SumFunc, + "textdecodebase64": funcs.TextDecodeBase64Func, + "textencodebase64": funcs.TextEncodeBase64Func, + "timestamp": funcs.TimestampFunc, "timeadd": stdlib.TimeAddFunc, + "timecmp": funcs.TimeCmpFunc, "title": stdlib.TitleFunc, - "tostring": funcs2.MakeToFunc(cty.String), - "tonumber": funcs2.MakeToFunc(cty.Number), - "tobool": funcs2.MakeToFunc(cty.Bool), - "toset": funcs2.MakeToFunc(cty.Set(cty.DynamicPseudoType)), - "tolist": funcs2.MakeToFunc(cty.List(cty.DynamicPseudoType)), - "tomap": funcs2.MakeToFunc(cty.Map(cty.DynamicPseudoType)), - "transpose": funcs2.TransposeFunc, + "tostring": funcs.MakeToFunc(cty.String), + "tonumber": funcs.MakeToFunc(cty.Number), + "tobool": funcs.MakeToFunc(cty.Bool), + "toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)), + "tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)), + "tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)), + "transpose": funcs.TransposeFunc, "trim": stdlib.TrimFunc, "trimprefix": stdlib.TrimPrefixFunc, "trimspace": stdlib.TrimSpaceFunc, "trimsuffix": stdlib.TrimSuffixFunc, "try": tryfunc.TryFunc, "upper": stdlib.UpperFunc, - "urlencode": funcs2.URLEncodeFunc, - "uuid": funcs2.UUIDFunc, - "uuidv5": funcs2.UUIDV5Func, + "urlencode": funcs.URLEncodeFunc, + "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, "values": stdlib.ValuesFunc, "yamldecode": ctyyaml.YAMLDecodeFunc, "yamlencode": ctyyaml.YAMLEncodeFunc, From a58b731ee4e4f0dda1dc14453c94e39526d2f537 Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Fri, 29 Mar 2024 17:32:19 +0700 Subject: [PATCH 2/3] skip tf funcs for golangci --- .golangci.yaml | 2 ++ pkg/iac/scanners/terraform/parser/funcs/filesystem.go | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 1ab912bad2b1..7be028f1e1ce 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -94,6 +94,8 @@ run: - ".*_test.go$" - "integration/*" - "examples/*" + skip-dirs: + - "pkg/iac/scanners/terraform/parser/funcs" # copies of Terraform functions issues: exclude-rules: diff --git a/pkg/iac/scanners/terraform/parser/funcs/filesystem.go b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go index 64e1c8b0ed19..a53e975443f6 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/filesystem.go +++ b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go @@ -205,7 +205,7 @@ func MakeFileExistsFunc(target fs.FS, baseDir string) function.Function { path = filepath.Join(baseDir, path) } - // Trivy uses a vitrual file system + // Trivy uses a virtual file system path = filepath.ToSlash(path) fi, err := fs.Stat(target, path) @@ -253,7 +253,7 @@ func MakeFileSetFunc(target fs.FS, baseDir string) function.Function { // pattern is canonical for the host OS. The joined path is // automatically cleaned during this operation. pattern = filepath.Join(path, pattern) - // Trivy uses a vitrual file system + // Trivy uses a virtual file system path = filepath.ToSlash(path) matches, err := doublestar.Glob(target, pattern) @@ -367,7 +367,7 @@ func openFile(target fs.FS, baseDir, path string) (fs.File, error) { path = filepath.Join(baseDir, path) } - // Trivy uses a vitrual file system + // Trivy uses a virtual file system path = filepath.ToSlash(path) if target != nil { From 4695a2858f42a7fa62782be06bfb35e79083a11e Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Wed, 3 Apr 2024 13:01:24 +0700 Subject: [PATCH 3/3] fix typos --- pkg/iac/scanners/terraform/parser/funcs/cidr.go | 6 +++--- pkg/iac/scanners/terraform/parser/funcs/crypto.go | 10 +++++----- pkg/iac/scanners/terraform/parser/funcs/ip.go | 2 +- pkg/iac/scanners/terraform/parser/funcs/number.go | 8 ++++---- pkg/iac/scanners/terraform/parser/funcs/string.go | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/funcs/cidr.go b/pkg/iac/scanners/terraform/parser/funcs/cidr.go index 910f653779ec..23b1c7be0d45 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/cidr.go +++ b/pkg/iac/scanners/terraform/parser/funcs/cidr.go @@ -11,7 +11,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -// CidrHostFunc contructs a function that calculates a full host IP address +// CidrHostFunc constructs a function that calculates a full host IP address // within a given IP network address prefix. var CidrHostFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -45,7 +45,7 @@ var CidrHostFunc = function.New(&function.Spec{ }, }) -// CidrNetmaskFunc contructs a function that converts an IPv4 address prefix given +// CidrNetmaskFunc constructs a function that converts an IPv4 address prefix given // in CIDR notation into a subnet mask address. var CidrNetmaskFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -70,7 +70,7 @@ var CidrNetmaskFunc = function.New(&function.Spec{ }, }) -// CidrSubnetFunc contructs a function that calculates a subnet address within +// CidrSubnetFunc constructs a function that calculates a subnet address within // a given IP network address prefix. var CidrSubnetFunc = function.New(&function.Spec{ Params: []function.Parameter{ diff --git a/pkg/iac/scanners/terraform/parser/funcs/crypto.go b/pkg/iac/scanners/terraform/parser/funcs/crypto.go index c2c52842cf7f..894da1280c1a 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/crypto.go +++ b/pkg/iac/scanners/terraform/parser/funcs/crypto.go @@ -124,7 +124,7 @@ var BcryptFunc = function.New(&function.Spec{ input := args[0].AsString() out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost) if err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("error occured generating password %s", err.Error()) + return cty.UnknownVal(cty.String), fmt.Errorf("error occurred generating password %s", err.Error()) } return cty.StringVal(string(out)), nil @@ -170,7 +170,7 @@ var RsaDecryptFunc = function.New(&function.Spec{ case asn1.SyntaxError: errStr = strings.ReplaceAll(e.Error(), "asn1: syntax error", "invalid ASN1 data in the given private key") case asn1.StructuralError: - errStr = strings.ReplaceAll(e.Error(), "asn1: struture error", "invalid ASN1 data in the given private key") + errStr = strings.ReplaceAll(e.Error(), "asn1: structure error", "invalid ASN1 data in the given private key") default: errStr = fmt.Sprintf("invalid private key: %s", e) } @@ -190,7 +190,7 @@ var RsaDecryptFunc = function.New(&function.Spec{ }, }) -// Sha1Func contructs a function that computes the SHA1 hash of a given string +// Sha1Func constructs a function that computes the SHA1 hash of a given string // and encodes it with hexadecimal digits. var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString) @@ -200,7 +200,7 @@ func MakeFileSha1Func(target fs.FS, baseDir string) function.Function { return makeFileHashFunction(target, baseDir, sha1.New, hex.EncodeToString) } -// Sha256Func contructs a function that computes the SHA256 hash of a given string +// Sha256Func constructs a function that computes the SHA256 hash of a given string // and encodes it with hexadecimal digits. var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString) @@ -210,7 +210,7 @@ func MakeFileSha256Func(target fs.FS, baseDir string) function.Function { return makeFileHashFunction(target, baseDir, sha256.New, hex.EncodeToString) } -// Sha512Func contructs a function that computes the SHA512 hash of a given string +// Sha512Func constructs a function that computes the SHA512 hash of a given string // and encodes it with hexadecimal digits. var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString) diff --git a/pkg/iac/scanners/terraform/parser/funcs/ip.go b/pkg/iac/scanners/terraform/parser/funcs/ip.go index 42bcfba4e618..d1cf0352e95f 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/ip.go +++ b/pkg/iac/scanners/terraform/parser/funcs/ip.go @@ -88,7 +88,7 @@ func parseIPv4(s string) IP { // backwards-incompatible change for Terraform users, who might have // already written modules with leading zeroes. // - //if c > 1 && s[0] == '0' { + // if c > 1 && s[0] == '0' { // // Reject non-zero components with leading zeroes. // return nil //} diff --git a/pkg/iac/scanners/terraform/parser/funcs/number.go b/pkg/iac/scanners/terraform/parser/funcs/number.go index 3fd606792177..60ebd660bf18 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/number.go +++ b/pkg/iac/scanners/terraform/parser/funcs/number.go @@ -10,7 +10,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -// LogFunc contructs a function that returns the logarithm of a given number in a given base. +// LogFunc constructs a function that returns the logarithm of a given number in a given base. var LogFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -39,7 +39,7 @@ var LogFunc = function.New(&function.Spec{ }, }) -// PowFunc contructs a function that returns the logarithm of a given number in a given base. +// PowFunc constructs a function that returns the logarithm of a given number in a given base. var PowFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -68,7 +68,7 @@ var PowFunc = function.New(&function.Spec{ }, }) -// SignumFunc contructs a function that returns the closest whole number greater +// SignumFunc constructs a function that returns the closest whole number greater // than or equal to the given value. var SignumFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -95,7 +95,7 @@ var SignumFunc = function.New(&function.Spec{ }, }) -// ParseIntFunc contructs a function that parses a string argument and returns an integer of the specified base. +// ParseIntFunc constructs a function that parses a string argument and returns an integer of the specified base. var ParseIntFunc = function.New(&function.Spec{ Params: []function.Parameter{ { diff --git a/pkg/iac/scanners/terraform/parser/funcs/string.go b/pkg/iac/scanners/terraform/parser/funcs/string.go index 7c3150047eb6..f859c7af7a31 100644 --- a/pkg/iac/scanners/terraform/parser/funcs/string.go +++ b/pkg/iac/scanners/terraform/parser/funcs/string.go @@ -87,7 +87,7 @@ var EndsWithFunc = function.New(&function.Spec{ }) // ReplaceFunc constructs a function that searches a given string for another -// given substring, and replaces each occurence with a given replacement string. +// given substring, and replaces each occurrence with a given replacement string. var ReplaceFunc = function.New(&function.Spec{ Params: []function.Parameter{ {