diff --git a/changes/unreleased/Added-20230629-110316.yaml b/changes/unreleased/Added-20230629-110316.yaml new file mode 100644 index 00000000..2faffa56 --- /dev/null +++ b/changes/unreleased/Added-20230629-110316.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Add support for a few ARM template string functions +time: 2023-06-29T11:03:16.471829+01:00 diff --git a/go.mod b/go.mod index 3437450d..604ed03d 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.0 + github.com/vincent-petithory/dataurl v1.0.0 github.com/zclconf/go-cty v1.12.1 github.com/zclconf/go-cty-yaml v1.0.2 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e diff --git a/go.sum b/go.sum index 5aa385d5..7f5fc80d 100644 --- a/go.sum +++ b/go.sum @@ -509,6 +509,8 @@ github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BG github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= +github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= diff --git a/pkg/input/arm/eval.go b/pkg/input/arm/eval.go index 3446cd36..8009aca7 100644 --- a/pkg/input/arm/eval.go +++ b/pkg/input/arm/eval.go @@ -31,10 +31,15 @@ type Function func(e *EvaluationContext, args ...interface{}) (interface{}, erro func BuiltinFunctions() map[string]Function { return map[string]Function{ - "concat": concatImpl, - "resourceGroup": resourceGroupImpl, - "resourceId": resourceIDImpl, - "variables": variablesImpl, + "base64": oneStringArg(base64Impl), + "base64ToString": oneStringArg(base64ToStringImpl), + "concat": concatImpl, + "dataUri": oneStringArg(dataURIImpl), + "dataUriToString": oneStringArg(dataURIToStringImpl), + "first": oneStringArg(firstImpl), + "resourceGroup": resourceGroupImpl, + "resourceId": resourceIDImpl, + "variables": variablesImpl, } } diff --git a/pkg/input/arm/functions_string.go b/pkg/input/arm/functions_string.go index cc85f56f..2040b3c1 100644 --- a/pkg/input/arm/functions_string.go +++ b/pkg/input/arm/functions_string.go @@ -14,10 +14,45 @@ package arm -import "fmt" +import ( + "encoding/base64" + "fmt" -// Note that concat can operate on arrays too, we just haven't implemented -// support for this yet. + "github.com/vincent-petithory/dataurl" +) + +// Functions from https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions-string#base64 + +func oneStringArg(f func(string) (interface{}, error)) Function { + return func(e *EvaluationContext, args ...interface{}) (interface{}, error) { + strargs, err := assertAllType[string](args...) + if err != nil { + return nil, err + } + if len(strargs) != 1 { + return nil, fmt.Errorf("expected 1 arg, got %d", len(strargs)) + } + return f(strargs[0]) + } +} + +func base64Impl(arg string) (interface{}, error) { + return base64.StdEncoding.EncodeToString([]byte(arg)), nil +} + +func base64ToStringImpl(arg string) (interface{}, error) { + decoded, err := base64.StdEncoding.DecodeString(arg) + if err != nil { + return nil, fmt.Errorf("error decoding base64: %w", err) + } + return string(decoded), nil +} + +// TODO base64ToJson returns an object. We can implement this when we introduce +// object variable support. + +// TODO concat can operate on arrays too, we just haven't implemented support +// for this yet. func concatImpl(e *EvaluationContext, args ...interface{}) (interface{}, error) { res := "" for _, arg := range args { @@ -29,3 +64,29 @@ func concatImpl(e *EvaluationContext, args ...interface{}) (interface{}, error) } return res, nil } + +// TODO contains can operate on arrays and objects, and returns a boolean. We +// haven't implemented support for these types yet. + +func dataURIImpl(arg string) (interface{}, error) { + return dataurl.EncodeBytes([]byte(arg)), nil +} + +func dataURIToStringImpl(arg string) (interface{}, error) { + decoded, err := dataurl.DecodeString(arg) + if err != nil { + return nil, fmt.Errorf("error decoding dataUri: %w", err) + } + return string(decoded.Data), nil +} + +// TODO empty returns a boolean, a type we haven't implemented support for yet +// TODO endsWith returns a boolean, a type we haven't implemented support for yet + +// TODO first can also operate on arrays, a type we haven't implemented support +// for yet +func firstImpl(arg string) (interface{}, error) { + return string([]rune(arg)[0]), nil +} + +// TODO implement format after adding integer and boolean type support diff --git a/pkg/input/arm/functions_string_test.go b/pkg/input/arm/functions_string_test.go new file mode 100644 index 00000000..c299154c --- /dev/null +++ b/pkg/input/arm/functions_string_test.go @@ -0,0 +1,62 @@ +// © 2022-2023 Snyk Limited All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package arm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBase64(t *testing.T) { + res, err := base64Impl("foo") + require.NoError(t, err) + require.Equal(t, "Zm9v", res) +} + +func TestBase64ToString(t *testing.T) { + res, err := base64ToStringImpl("Zm9v") + require.NoError(t, err) + require.Equal(t, "foo", res) +} + +func TestConcat(t *testing.T) { + res, err := concatImpl(nil, "foo", "bar") + require.NoError(t, err) + require.Equal(t, "foobar", res) +} + +func TestDataURI(t *testing.T) { + res, err := dataURIImpl("Hello") + require.NoError(t, err) + + // Behaves slightly differently than the example in https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions-string#TestDataURI + // Note the absence of a hyphen in "data:text/plain;charset=utf8;base64,SGVsbG8=" + // It's not clear whether or not this is a problem, it depends on how tolerant + // of different representations of charset names Azure is. + require.Equal(t, "data:text/plain;charset=utf-8;base64,SGVsbG8=", res) +} + +func TestDataURIToString(t *testing.T) { + res, err := dataURIToStringImpl("data:;base64,SGVsbG8sIFdvcmxkIQ==") + require.NoError(t, err) + require.Equal(t, "Hello, World!", res) +} + +func TestFirst(t *testing.T) { + res, err := firstImpl("Hello") + require.NoError(t, err) + require.Equal(t, "H", res) +}