From a8c03d0ecc95cc86a89dfd5384941aa9bde4d93a Mon Sep 17 00:00:00 2001 From: Raj Nishtala <113392743+rnishtala-sumo@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:07:29 -0400 Subject: [PATCH] [pkg/ottl] Added ottl functions for hashing strings (#22968) * [pkg/ottl] Added ottl functions for hashing strings * [pkg/ottl] Return nil on errors * Documentation and test changes for the hash functions Co-authored-by: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> --------- Co-authored-by: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com> --- .chloggen/ottl_hash_functions.yaml | 20 +++++++ pkg/ottl/ottlfuncs/README.md | 64 ++++++++++++++++++++ pkg/ottl/ottlfuncs/func_fnv.go | 47 +++++++++++++++ pkg/ottl/ottlfuncs/func_fnv_test.go | 82 ++++++++++++++++++++++++++ pkg/ottl/ottlfuncs/func_sha1.go | 48 +++++++++++++++ pkg/ottl/ottlfuncs/func_sha1_test.go | 82 ++++++++++++++++++++++++++ pkg/ottl/ottlfuncs/func_sha256.go | 48 +++++++++++++++ pkg/ottl/ottlfuncs/func_sha256_test.go | 82 ++++++++++++++++++++++++++ 8 files changed, 473 insertions(+) create mode 100755 .chloggen/ottl_hash_functions.yaml create mode 100644 pkg/ottl/ottlfuncs/func_fnv.go create mode 100644 pkg/ottl/ottlfuncs/func_fnv_test.go create mode 100644 pkg/ottl/ottlfuncs/func_sha1.go create mode 100644 pkg/ottl/ottlfuncs/func_sha1_test.go create mode 100644 pkg/ottl/ottlfuncs/func_sha256.go create mode 100644 pkg/ottl/ottlfuncs/func_sha256_test.go diff --git a/.chloggen/ottl_hash_functions.yaml b/.chloggen/ottl_hash_functions.yaml new file mode 100755 index 000000000000..2da65e744da2 --- /dev/null +++ b/.chloggen/ottl_hash_functions.yaml @@ -0,0 +1,20 @@ +# Use this changelog template to create an entry for release notes. +# If your change doesn't affect end users, such as a test fix or a tooling change, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: pkg/ottl + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Add hash converters/functions for OTTL" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [22725] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/pkg/ottl/ottlfuncs/README.md b/pkg/ottl/ottlfuncs/README.md index 8860fe098823..485d175bad2d 100644 --- a/pkg/ottl/ottlfuncs/README.md +++ b/pkg/ottl/ottlfuncs/README.md @@ -263,10 +263,13 @@ Unlike functions, they do not modify any input telemetry and always return a val Available Converters: - [Concat](#concat) - [ConvertCase](#convertcase) +- [FNV](#fnv) - [Int](#int) - [IsMatch](#ismatch) - [Log](#log) - [ParseJSON](#parsejson) +- [SHA1](#sha1) +- [SHA256](#sha256) - [SpanID](#spanid) - [Split](#split) - [TraceID](#traceid) @@ -316,6 +319,25 @@ Examples: - `ConvertCase(metric.name, "snake")` +### FNV + +`FNV(value)` + +The `FNV` Converter converts the `value` to an FNV hash/digest. + +The returned type is int64. + +`value` is either a path expression to a string telemetry field or a literal string. If `value` is another type an error is returned. + +If an error occurs during hashing it will be returned. + +Examples: + +- `FNV(attributes["device.name"])` + + +- `FNV("name")` + ### Int `Int(value)` @@ -424,6 +446,48 @@ Examples: - `ParseJSON(body)` +### SHA1 + +`SHA1(value)` + +The `SHA1` Converter converts the `value` to a sha1 hash/digest. + +The returned type is string. + +`value` is either a path expression to a string telemetry field or a literal string. If `value` is another type an error is returned. + +If an error occurs during hashing it will be returned. + +Examples: + +- `SHA1(attributes["device.name"])` + + +- `SHA1("name")` + +**Note:** According to the National Institute of Standards and Technology (NIST), SHA1 is no longer a recommended hash function. It should be avoided except when required for compatibility. New uses should prefer FNV whenever possible. + +### SHA256 + +`SHA256(value)` + +The `SHA256` Converter converts the `value` to a sha256 hash/digest. + +The returned type is string. + +`value` is either a path expression to a string telemetry field or a literal string. If `value` is another type an error is returned. + +If an error occurs during hashing it will be returned. + +Examples: + +- `SHA256(attributes["device.name"])` + + +- `SHA256("name")` + +**Note:** According to the National Institute of Standards and Technology (NIST), SHA256 is no longer a recommended hash function. It should be avoided except when required for compatibility. New uses should prefer FNV whenever possible. + ### SpanID `SpanID(bytes)` diff --git a/pkg/ottl/ottlfuncs/func_fnv.go b/pkg/ottl/ottlfuncs/func_fnv.go new file mode 100644 index 000000000000..026a4d1f1b02 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_fnv.go @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "fmt" + "hash/fnv" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type FnvArguments[K any] struct { + Target ottl.StringGetter[K] `ottlarg:"0"` +} + +func NewFnvFactory[K any]() ottl.Factory[K] { + return ottl.NewFactory("FNV", &FnvArguments[K]{}, createFnvFunction[K]) +} + +func createFnvFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*FnvArguments[K]) + + if !ok { + return nil, fmt.Errorf("FNVFactory args must be of type *FnvArguments[K]") + } + + return FNVHashString(args.Target) +} + +func FNVHashString[K any](target ottl.StringGetter[K]) (ottl.ExprFunc[K], error) { + + return func(ctx context.Context, tCtx K) (interface{}, error) { + val, err := target.Get(ctx, tCtx) + if err != nil { + return nil, err + } + hash := fnv.New64a() + _, err = hash.Write([]byte(val)) + if err != nil { + return nil, err + } + hashValue := hash.Sum64() + return int64(hashValue), nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_fnv_test.go b/pkg/ottl/ottlfuncs/func_fnv_test.go new file mode 100644 index 000000000000..3a0e4f6f83c5 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_fnv_test.go @@ -0,0 +1,82 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_FNV(t *testing.T) { + tests := []struct { + name string + value interface{} + expected interface{} + err bool + }{ + { + name: "string", + value: "hello world", + expected: int64(8618312879776256743), + }, + { + name: "empty string", + value: "", + expected: int64(-3750763034362895579), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := FNVHashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, result) + }) + } +} + +func Test_FNVError(t *testing.T) { + tests := []struct { + name string + value interface{} + err bool + expectedError string + }{ + { + name: "non-string", + value: 10, + expectedError: "expected string but got int", + }, + { + name: "nil", + value: nil, + expectedError: "expected string but got nil", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := FNVHashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + _, err = exprFunc(nil, nil) + assert.ErrorContains(t, err, tt.expectedError) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/func_sha1.go b/pkg/ottl/ottlfuncs/func_sha1.go new file mode 100644 index 000000000000..f8eb721b935a --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_sha1.go @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "crypto/sha1" // #nosec + "encoding/hex" + "fmt" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type SHA1Arguments[K any] struct { + Target ottl.StringGetter[K] `ottlarg:"0"` +} + +func NewSHA1Factory[K any]() ottl.Factory[K] { + return ottl.NewFactory("SHA1", &SHA1Arguments[K]{}, createSHA1Function[K]) +} + +func createSHA1Function[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*SHA1Arguments[K]) + + if !ok { + return nil, fmt.Errorf("SHA1Factory args must be of type *SHA1Arguments[K]") + } + + return SHA1HashString(args.Target) +} + +func SHA1HashString[K any](target ottl.StringGetter[K]) (ottl.ExprFunc[K], error) { + + return func(ctx context.Context, tCtx K) (interface{}, error) { + val, err := target.Get(ctx, tCtx) + if err != nil { + return nil, err + } + hash := sha1.New() // #nosec + _, err = hash.Write([]byte(val)) + if err != nil { + return nil, err + } + hashValue := hex.EncodeToString(hash.Sum(nil)) + return hashValue, nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_sha1_test.go b/pkg/ottl/ottlfuncs/func_sha1_test.go new file mode 100644 index 000000000000..9b2d384ed095 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_sha1_test.go @@ -0,0 +1,82 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_SHA1(t *testing.T) { + tests := []struct { + name string + value interface{} + expected interface{} + err bool + }{ + { + name: "string", + value: "hello world", + expected: "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", + }, + { + name: "empty string", + value: "", + expected: "da39a3ee5e6b4b0d3255bfef95601890afd80709", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := SHA1HashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, result) + }) + } +} + +func Test_SHA1Error(t *testing.T) { + tests := []struct { + name string + value interface{} + err bool + expectedError string + }{ + { + name: "non-string", + value: 10, + expectedError: "expected string but got int", + }, + { + name: "nil", + value: nil, + expectedError: "expected string but got nil", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := SHA1HashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + _, err = exprFunc(nil, nil) + assert.ErrorContains(t, err, tt.expectedError) + }) + } +} diff --git a/pkg/ottl/ottlfuncs/func_sha256.go b/pkg/ottl/ottlfuncs/func_sha256.go new file mode 100644 index 000000000000..fc8a9259b311 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_sha256.go @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs" + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +type SHA256Arguments[K any] struct { + Target ottl.StringGetter[K] `ottlarg:"0"` +} + +func NewSHA256Factory[K any]() ottl.Factory[K] { + return ottl.NewFactory("SHA256", &SHA256Arguments[K]{}, createSHA256Function[K]) +} + +func createSHA256Function[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) { + args, ok := oArgs.(*SHA256Arguments[K]) + + if !ok { + return nil, fmt.Errorf("SHA256Factory args must be of type *SHA256Arguments[K]") + } + + return SHA256HashString(args.Target) +} + +func SHA256HashString[K any](target ottl.StringGetter[K]) (ottl.ExprFunc[K], error) { + + return func(ctx context.Context, tCtx K) (interface{}, error) { + val, err := target.Get(ctx, tCtx) + if err != nil { + return nil, err + } + hash := sha256.New() + _, err = hash.Write([]byte(val)) + if err != nil { + return nil, err + } + hashValue := hex.EncodeToString(hash.Sum(nil)) + return hashValue, nil + }, nil +} diff --git a/pkg/ottl/ottlfuncs/func_sha256_test.go b/pkg/ottl/ottlfuncs/func_sha256_test.go new file mode 100644 index 000000000000..5d308f2faed5 --- /dev/null +++ b/pkg/ottl/ottlfuncs/func_sha256_test.go @@ -0,0 +1,82 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ottlfuncs + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl" +) + +func Test_SHA256(t *testing.T) { + tests := []struct { + name string + value interface{} + expected interface{} + err bool + }{ + { + name: "string", + value: "hello world", + expected: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + { + name: "empty string", + value: "", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := SHA256HashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + result, err := exprFunc(nil, nil) + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, result) + }) + } +} + +func Test_SHA256Error(t *testing.T) { + tests := []struct { + name string + value interface{} + err bool + expectedError string + }{ + { + name: "non-string", + value: 10, + expectedError: "expected string but got int", + }, + { + name: "nil", + value: nil, + expectedError: "expected string but got nil", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exprFunc, err := SHA256HashString[interface{}](&ottl.StandardStringGetter[interface{}]{ + Getter: func(context.Context, interface{}) (interface{}, error) { + return tt.value, nil + }, + }) + assert.NoError(t, err) + _, err = exprFunc(nil, nil) + assert.ErrorContains(t, err, tt.expectedError) + }) + } +}