From 4c251bcffd65f412ecd23a23d0c9a5088edfa1ad Mon Sep 17 00:00:00 2001
From: Sebastian Widmer <widmer.sebastian@gmail.com>
Date: Mon, 29 Nov 2021 17:13:07 +0100
Subject: [PATCH] Add `pattern_match` custom JMESPath function analogous to
 `regex_match` (#2717)

* Add `pattern_match` custom JMESPath function analogous to `regex_match`

Signed-off-by: Sebastian Widmer <sebastian.widmer@vshn.net>

* Add CLI test for the custom `pattern_match` function

Signed-off-by: Sebastian Widmer <sebastian.widmer@vshn.net>
---
 pkg/engine/jmespath/functions.go              | 24 +++++++++++++++++++
 pkg/engine/jmespath/functions_test.go         | 24 +++++++++++++++++++
 test/cli/test/custom-functions/policy.yaml    | 22 +++++++++++++++++
 test/cli/test/custom-functions/resources.yaml | 18 ++++++++++++--
 test/cli/test/custom-functions/test.yaml      | 14 +++++++++--
 5 files changed, 98 insertions(+), 4 deletions(-)

diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index 746792c45b..9fb00d6c8e 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	gojmespath "github.com/jmespath/go-jmespath"
+	"github.com/minio/pkg/wildcard"
 )
 
 var (
@@ -39,6 +40,7 @@ var (
 	regexReplaceAll        = "regex_replace_all"
 	regexReplaceAllLiteral = "regex_replace_all_literal"
 	regexMatch             = "regex_match"
+	patternMatch           = "pattern_match"
 	labelMatch             = "label_match"
 	add                    = "add"
 	subtract               = "subtract"
@@ -149,6 +151,14 @@ func getFunctions() []*gojmespath.FunctionEntry {
 			},
 			Handler: jpRegexMatch,
 		},
+		{
+			Name: patternMatch,
+			Arguments: []ArgSpec{
+				{Types: []JpType{JpString}},
+				{Types: []JpType{JpString, JpNumber}},
+			},
+			Handler: jpPatternMatch,
+		},
 		{
 			// Validates if label (param1) would match pod/host/etc labels (param2)
 			Name: labelMatch,
@@ -420,6 +430,20 @@ func jpRegexMatch(arguments []interface{}) (interface{}, error) {
 	return regexp.Match(regex.String(), []byte(src))
 }
 
+func jpPatternMatch(arguments []interface{}) (interface{}, error) {
+	pattern, err := validateArg(regexMatch, arguments, 0, reflect.String)
+	if err != nil {
+		return nil, err
+	}
+
+	src, err := ifaceToString(arguments[1])
+	if err != nil {
+		return nil, fmt.Errorf(invalidArgumentTypeError, regexMatch, 2, "String or Real")
+	}
+
+	return wildcard.Match(pattern.String(), src), nil
+}
+
 func jpLabelMatch(arguments []interface{}) (interface{}, error) {
 	labelMap, ok := arguments[0].(map[string]interface{})
 
diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go
index b6a0d784bc..0809f491db 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -268,6 +268,30 @@ func Test_RegexMatchWithNumber(t *testing.T) {
 	assert.Equal(t, true, result)
 }
 
+func Test_PatternMatch(t *testing.T) {
+	data := make(map[string]interface{})
+	data["foo"] = "prefix-foo"
+
+	query, err := New("pattern_match('prefix-*', foo)")
+	assert.NilError(t, err)
+
+	result, err := query.Search(data)
+	assert.NilError(t, err)
+	assert.Equal(t, true, result)
+}
+
+func Test_PatternMatchWithNumber(t *testing.T) {
+	data := make(map[string]interface{})
+	data["foo"] = -12.0
+
+	query, err := New("pattern_match('12*', abs(foo))")
+	assert.NilError(t, err)
+
+	result, err := query.Search(data)
+	assert.NilError(t, err)
+	assert.Equal(t, true, result)
+}
+
 func Test_RegexReplaceAll(t *testing.T) {
 	resourceRaw := []byte(`
 	{
diff --git a/test/cli/test/custom-functions/policy.yaml b/test/cli/test/custom-functions/policy.yaml
index 15e6b1dafb..be2e77b4f6 100644
--- a/test/cli/test/custom-functions/policy.yaml
+++ b/test/cli/test/custom-functions/policy.yaml
@@ -17,3 +17,25 @@ spec:
         - key: "{{base64_decode(request.object.data.value)}}"
           operator: NotEquals
           value: "{{request.object.metadata.labels.value}}"
+---
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+  name: pattern-match
+spec:
+  validationFailureAction: enforce
+  background: false
+  rules:
+    - match:
+        all:
+          - resources:
+              kinds:
+                - Namespace
+      name: label-must-match-pattern
+      validate:
+        deny:
+          conditions:
+            all:
+              - key: "{{pattern_match('prefix-*', request.object.metadata.labels.value)}}"
+                operator: Equals
+                value: false
diff --git a/test/cli/test/custom-functions/resources.yaml b/test/cli/test/custom-functions/resources.yaml
index a050faac52..57f01097e8 100644
--- a/test/cli/test/custom-functions/resources.yaml
+++ b/test/cli/test/custom-functions/resources.yaml
@@ -1,7 +1,7 @@
 apiVersion: v1
 kind: Secret
 metadata:
-  name: test-match
+  name: base64-test-match
   labels:
     value: hello
 type: Opaque
@@ -11,9 +11,23 @@ data:
 apiVersion: v1
 kind: Secret
 metadata:
-  name: test-no-match
+  name: base64-test-no-match
   labels:
     value: hello
 type: Opaque
 data:
   value: Z29vZGJ5ZQ==
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: pattern-match-test-match
+  labels:
+    value: prefix-test
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: pattern-match-test-no-match
+  labels:
+    value: test
diff --git a/test/cli/test/custom-functions/test.yaml b/test/cli/test/custom-functions/test.yaml
index cf804c652a..996f9bfacf 100644
--- a/test/cli/test/custom-functions/test.yaml
+++ b/test/cli/test/custom-functions/test.yaml
@@ -6,11 +6,21 @@ resources:
 results:
   - policy: base64
     rule: secret-value-must-match-label
-    resource: test-match
+    resource: base64-test-match
     kind: Secret
     status: pass
   - policy: base64
     rule: secret-value-must-match-label
-    resource: test-no-match
+    resource: base64-test-no-match
     kind: Secret
     status: fail
+  - policy: pattern-match
+    rule: label-must-match-pattern
+    resource: pattern-match-test-match
+    kind: Namespace
+    status: pass
+  - policy: pattern-match
+    rule: label-must-match-pattern
+    resource: pattern-match-test-no-match
+    kind: Namespace
+    status: fail