diff --git a/pkg/json-engine/compiler.go b/pkg/json-engine/compiler.go index b5a7ec6c..d584f032 100644 --- a/pkg/json-engine/compiler.go +++ b/pkg/json-engine/compiler.go @@ -41,7 +41,7 @@ func (c *compiler) compileContextEntry( }, nil } -func (c *compiler) compileContext( +func (c *compiler) compileContextEntries( path *field.Path, compilers compilers.Compilers, in ...v1alpha1.ContextEntry, @@ -310,11 +310,11 @@ func (c *compiler) compileRule( path *field.Path, compilers compilers.Compilers, in v1alpha1.ValidatingRule, -) (func(any, binding.Bindings) []RuleResponse, error) { +) (func(any, binding.Bindings) *RuleResponse, error) { if in.Compiler != nil { compilers = compilers.WithDefaultCompiler(string(*in.Compiler)) } - context, err := c.compileContext(path.Child("context"), compilers, in.Context...) + context, err := c.compileContextEntries(path.Child("context"), compilers, in.Context...) if err != nil { return nil, err } @@ -334,24 +334,23 @@ func (c *compiler) compileRule( if err != nil { return nil, err } - // TODO: fix path - assert, err := c.compileAssert(nil, compilers, in.Assert) + assert, err := c.compileAssert(path.Child("assert"), compilers, in.Assert) if err != nil { return nil, err } - return func(resource any, bindings binding.Bindings) []RuleResponse { + return func(resource any, bindings binding.Bindings) *RuleResponse { // register context bindings bindings = context(resource, bindings) // process match clause if match != nil { if errs, err := match(resource, bindings); err != nil { - return []RuleResponse{{ + return &RuleResponse{ Rule: in, Timestamp: time.Now(), Identifier: identifier(resource, bindings), Feedback: feedback(resource, bindings), Error: err, - }} + } } else if len(errs) != 0 { // didn't match return nil @@ -360,13 +359,13 @@ func (c *compiler) compileRule( // process exclude clause if exclude != nil { if errs, err := exclude(resource, bindings); err != nil { - return []RuleResponse{{ + return &RuleResponse{ Rule: in, Timestamp: time.Now(), Identifier: identifier(resource, bindings), Feedback: feedback(resource, bindings), Error: err, - }} + } } else if len(errs) == 0 { // matched return nil @@ -375,20 +374,20 @@ func (c *compiler) compileRule( // evaluate assertions violations, err := assert(resource, bindings) if err != nil { - return []RuleResponse{{ + return &RuleResponse{ Rule: in, Timestamp: time.Now(), Identifier: identifier(resource, bindings), Feedback: feedback(resource, bindings), Error: err, - }} + } } - return []RuleResponse{{ + return &RuleResponse{ Rule: in, Timestamp: time.Now(), Identifier: identifier(resource, bindings), Feedback: feedback(resource, bindings), Violations: violations, - }} + } }, nil } diff --git a/pkg/json-engine/engine.go b/pkg/json-engine/engine.go index 016c11f6..e746bd96 100644 --- a/pkg/json-engine/engine.go +++ b/pkg/json-engine/engine.go @@ -26,18 +26,18 @@ func New() engine.Engine[Request, Response] { compilers := compilers.DefaultCompilers.WithDefaultCompiler(compilers.CompilerJP) ruleCompiler := compiler{} ruleEngine := builder. - Function(func(ctx context.Context, r ruleRequest) []RuleResponse { + Function(func(ctx context.Context, r ruleRequest) *RuleResponse { compilers := compilers if r.policy.Spec.Compiler != nil { compilers = compilers.WithDefaultCompiler(string(*r.policy.Spec.Compiler)) } compiled, err := ruleCompiler.compileRule(nil, compilers, r.rule) if err != nil { - return []RuleResponse{{ + return &RuleResponse{ Rule: r.rule, Timestamp: time.Now(), Error: err, - }} + } } bindings := r.bindings.Register("$rule", binding.NewBinding(r.rule)) return compiled(r.resource, bindings) @@ -49,12 +49,14 @@ func New() engine.Engine[Request, Response] { } bindings := r.bindings.Register("$policy", binding.NewBinding(r.policy)) for _, rule := range r.policy.Spec.Rules { - response.Rules = append(response.Rules, ruleEngine.Run(ctx, ruleRequest{ + if ruleResponse := ruleEngine.Run(ctx, ruleRequest{ policy: r.policy, rule: rule, resource: r.resource, bindings: bindings.Register("$rule", binding.NewBinding(rule)), - })...) + }); ruleResponse != nil { + response.Rules = append(response.Rules, *ruleResponse) + } } return response }) diff --git a/pkg/server/ui/dist/assets/data.json b/pkg/server/ui/dist/assets/data.json index c2800573..8214164d 100644 --- a/pkg/server/ui/dist/assets/data.json +++ b/pkg/server/ui/dist/assets/data.json @@ -3,7 +3,7 @@ { "category": "Dockerfile", "name": "check-dockerfile", - "policy": "apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n name: check-dockerfile\nspec:\n rules:\n - name: deny-external-calls\n assert:\n all:\n - message: \"HTTP calls are not allowed\"\n check:\n ~.(Stages[].Commands[].Args[].Value):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: \"HTTP calls are not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: \"curl is not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'curl')): false\n - message: \"wget is not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'wget')): false", + "policy": "apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n name: check-dockerfile\nspec:\n rules:\n - name: deny-external-calls\n assert:\n all:\n - message: HTTP calls are not allowed\n check:\n ~.(Stages[].Commands[].Args[].Value):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: HTTP calls are not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: curl is not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'curl')): false\n - message: wget is not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'wget')): false", "payload": "MetaArgs:\n- DefaultValue: '\"linux/amd64\"'\n Key: BUILD_PLATFORM\n ProvidedValue: null\n Value: '\"linux/amd64\"'\n- DefaultValue: '\"golang:1.20.6-alpine3.18\"'\n Key: BUILDER_IMAGE\n ProvidedValue: null\n Value: '\"golang:1.20.6-alpine3.18\"'\nStages:\n- As: builder\n BaseName: '\"golang:1.20.6-alpine3.18\"'\n Commands:\n - Name: WORKDIR\n Path: /\n - Chmod: \"\"\n Chown: \"\"\n DestPath: ./\n From: \"\"\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - .\n - Args:\n - Comment: \"\"\n Key: SIGNER_BINARY_LINK\n Value: '\"https://d2hvyiie56hcat.cloudfront.net/linux/amd64/plugin/latest/notation-aws-signer-plugin.zip\"'\n Name: ARG\n - Args:\n - Comment: \"\"\n Key: SIGNER_BINARY_FILE\n Value: '\"notation-aws-signer-plugin.zip\"'\n Name: ARG\n - CmdLine:\n - wget -O ${SIGNER_BINARY_FILE} ${SIGNER_BINARY_LINK}\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n - CmdLine:\n - apk update \u0026\u0026 apk add unzip \u0026\u0026 unzip -o ${SIGNER_BINARY_FILE}\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n - CmdLine:\n - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=\"-w -s\" -o kyverno-notation-aws\n .\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n Comment: \"\"\n From:\n Image: '\"golang:1.20.6-alpine3.18\"'\n Location:\n - End:\n Character: 0\n Line: 4\n Start:\n Character: 0\n Line: 4\n Name: builder\n Platform: $BUILD_PLATFORM\n SourceCode: FROM --platform=$BUILD_PLATFORM $BUILDER_IMAGE as builder\n- BaseName: gcr.io/distroless/static:nonroot\n Commands:\n - Name: WORKDIR\n Path: /\n - Env:\n - Key: PLUGINS_DIR\n Value: /plugins\n Name: ENV\n - Chmod: \"\"\n Chown: \"\"\n DestPath: plugins/com.amazonaws.signer.notation.plugin/notation-com.amazonaws.signer.notation.plugin\n From: builder\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - notation-com.amazonaws.signer.notation.plugin\n - Chmod: \"\"\n Chown: \"\"\n DestPath: kyverno-notation-aws\n From: builder\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - kyverno-notation-aws\n - CmdLine:\n - /kyverno-notation-aws\n Files: null\n Name: ENTRYPOINT\n PrependShell: false\n Comment: \"\"\n From:\n Image: gcr.io/distroless/static:nonroot\n Location:\n - End:\n Character: 0\n Line: 20\n Start:\n Character: 0\n Line: 20\n Name: \"\"\n Platform: \"\"\n SourceCode: FROM gcr.io/distroless/static:nonroot\n" }, { diff --git a/test/commands/scan/dockerfile/out.txt b/test/commands/scan/dockerfile/out.txt index 52640d51..7de427b2 100644 --- a/test/commands/scan/dockerfile/out.txt +++ b/test/commands/scan/dockerfile/out.txt @@ -4,7 +4,7 @@ Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - check-dockerfile / deny-external-calls / FAILED -> HTTP calls are not allowed - -> all[0].check.~.(Stages[].Commands[].Args[].Value)[0].(contains(@, 'https://') || contains(@, 'http://')): Invalid value: true: Expected value: false + -> assert.all[0].check.~.(Stages[].Commands[].Args[].Value)[0].(contains(@, 'https://') || contains(@, 'http://')): Invalid value: true: Expected value: false -> wget is not allowed - -> all[3].check.~.(Stages[].Commands[].CmdLine[])[0].(contains(@, 'wget')): Invalid value: true: Expected value: false + -> assert.all[3].check.~.(Stages[].Commands[].CmdLine[])[0].(contains(@, 'wget')): Invalid value: true: Expected value: false Done diff --git a/test/commands/scan/dockerfile/policy.yaml b/test/commands/scan/dockerfile/policy.yaml index 50188e85..6263a634 100644 --- a/test/commands/scan/dockerfile/policy.yaml +++ b/test/commands/scan/dockerfile/policy.yaml @@ -7,19 +7,19 @@ spec: - name: deny-external-calls assert: all: - - message: "HTTP calls are not allowed" + - message: HTTP calls are not allowed check: ~.(Stages[].Commands[].Args[].Value): (contains(@, 'https://') || contains(@, 'http://')): false - - message: "HTTP calls are not allowed" + - message: HTTP calls are not allowed check: ~.(Stages[].Commands[].CmdLine[]): (contains(@, 'https://') || contains(@, 'http://')): false - - message: "curl is not allowed" + - message: curl is not allowed check: ~.(Stages[].Commands[].CmdLine[]): (contains(@, 'curl')): false - - message: "wget is not allowed" + - message: wget is not allowed check: ~.(Stages[].Commands[].CmdLine[]): (contains(@, 'wget')): false \ No newline at end of file diff --git a/test/commands/scan/payload-yaml/out.txt b/test/commands/scan/payload-yaml/out.txt index 5e4ed51b..757bf5e8 100644 --- a/test/commands/scan/payload-yaml/out.txt +++ b/test/commands/scan/payload-yaml/out.txt @@ -4,5 +4,5 @@ Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - required-s3-tags / require-team-tag / aws_s3_bucket.example FAILED -> Bucket `example` (aws_s3_bucket.example) does not have the required tags {"Team":"Kyverno"} - -> all[0].check.values.tags: Invalid value: map[string]interface {}{"Environment":"Dev", "Name":"My bucket"}: Expected value: map[string]interface {}{"Team":"Kyverno"} + -> assert.all[0].check.values.tags: Invalid value: map[string]interface {}{"Environment":"Dev", "Name":"My bucket"}: Expected value: map[string]interface {}{"Team":"Kyverno"} Done diff --git a/test/commands/scan/pod-no-latest/out.txt b/test/commands/scan/pod-no-latest/out.txt index 08b0cc3b..6919c5c6 100644 --- a/test/commands/scan/pod-no-latest/out.txt +++ b/test/commands/scan/pod-no-latest/out.txt @@ -3,13 +3,13 @@ Loading payload ... Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - test / pod-no-latest / webserver FAILED - -> all[0].check.spec.~foo.containers->foos[0].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false - -> all[0].check.spec.~foo.containers->foos[1].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false - -> all[0].check.spec.~foo.containers->foos[2].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false - -> all[1].check.spec.~.containers->foo[0].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false - -> all[1].check.spec.~.containers->foo[1].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false - -> all[1].check.spec.~.containers->foo[2].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false - -> all[2].check.~index.(spec.containers[*].image)->images[0].(ends_with(@, ':latest')): Invalid value: true: Expected value: false - -> all[2].check.~index.(spec.containers[*].image)->images[1].(ends_with(@, ':latest')): Invalid value: true: Expected value: false - -> all[2].check.~index.(spec.containers[*].image)->images[2].(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[0].check.spec.~foo.containers->foos[0].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false + -> assert.all[0].check.spec.~foo.containers->foos[1].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false + -> assert.all[0].check.spec.~foo.containers->foos[2].(at($foos, $foo).image)->foo.(ends_with($foo, $tag)): Invalid value: true: Expected value: false + -> assert.all[1].check.spec.~.containers->foo[0].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[1].check.spec.~.containers->foo[1].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[1].check.spec.~.containers->foo[2].image.(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[2].check.~index.(spec.containers[*].image)->images[0].(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[2].check.~index.(spec.containers[*].image)->images[1].(ends_with(@, ':latest')): Invalid value: true: Expected value: false + -> assert.all[2].check.~index.(spec.containers[*].image)->images[2].(ends_with(@, ':latest')): Invalid value: true: Expected value: false Done diff --git a/test/commands/scan/tf-plan/out.txt b/test/commands/scan/tf-plan/out.txt index 5e4ed51b..757bf5e8 100644 --- a/test/commands/scan/tf-plan/out.txt +++ b/test/commands/scan/tf-plan/out.txt @@ -4,5 +4,5 @@ Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - required-s3-tags / require-team-tag / aws_s3_bucket.example FAILED -> Bucket `example` (aws_s3_bucket.example) does not have the required tags {"Team":"Kyverno"} - -> all[0].check.values.tags: Invalid value: map[string]interface {}{"Environment":"Dev", "Name":"My bucket"}: Expected value: map[string]interface {}{"Team":"Kyverno"} + -> assert.all[0].check.values.tags: Invalid value: map[string]interface {}{"Environment":"Dev", "Name":"My bucket"}: Expected value: map[string]interface {}{"Team":"Kyverno"} Done diff --git a/test/commands/scan/tf-s3/out.txt b/test/commands/scan/tf-s3/out.txt index 1f6f0e3b..da1645af 100644 --- a/test/commands/scan/tf-s3/out.txt +++ b/test/commands/scan/tf-s3/out.txt @@ -3,5 +3,5 @@ Loading payload ... Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - s3 / check-tags / FAILED - -> all[0].check.planned_values.root_module.~.resources[0].values.(keys(tags_all)).(contains(@, 'Team')): Invalid value: false: Expected value: true + -> assert.all[0].check.planned_values.root_module.~.resources[0].values.(keys(tags_all)).(contains(@, 'Team')): Invalid value: false: Expected value: true Done diff --git a/test/commands/scan/wildcard/out.txt b/test/commands/scan/wildcard/out.txt index 1e8e6126..4c418c70 100644 --- a/test/commands/scan/wildcard/out.txt +++ b/test/commands/scan/wildcard/out.txt @@ -3,5 +3,5 @@ Loading payload ... Pre processing ... Running ( evaluating 1 resource against 1 policy ) ... - required-s3-tags / require-team-tag / bucket1 FAILED - -> all[0].check.tags.(wildcard('?*', Team)): Invalid value: true: Expected value: false + -> assert.all[0].check.tags.(wildcard('?*', Team)): Invalid value: true: Expected value: false Done diff --git a/website/playground/assets/data.json b/website/playground/assets/data.json index c2800573..8214164d 100644 --- a/website/playground/assets/data.json +++ b/website/playground/assets/data.json @@ -3,7 +3,7 @@ { "category": "Dockerfile", "name": "check-dockerfile", - "policy": "apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n name: check-dockerfile\nspec:\n rules:\n - name: deny-external-calls\n assert:\n all:\n - message: \"HTTP calls are not allowed\"\n check:\n ~.(Stages[].Commands[].Args[].Value):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: \"HTTP calls are not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: \"curl is not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'curl')): false\n - message: \"wget is not allowed\"\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'wget')): false", + "policy": "apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n name: check-dockerfile\nspec:\n rules:\n - name: deny-external-calls\n assert:\n all:\n - message: HTTP calls are not allowed\n check:\n ~.(Stages[].Commands[].Args[].Value):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: HTTP calls are not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'https://') || contains(@, 'http://')): false\n - message: curl is not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'curl')): false\n - message: wget is not allowed\n check:\n ~.(Stages[].Commands[].CmdLine[]):\n (contains(@, 'wget')): false", "payload": "MetaArgs:\n- DefaultValue: '\"linux/amd64\"'\n Key: BUILD_PLATFORM\n ProvidedValue: null\n Value: '\"linux/amd64\"'\n- DefaultValue: '\"golang:1.20.6-alpine3.18\"'\n Key: BUILDER_IMAGE\n ProvidedValue: null\n Value: '\"golang:1.20.6-alpine3.18\"'\nStages:\n- As: builder\n BaseName: '\"golang:1.20.6-alpine3.18\"'\n Commands:\n - Name: WORKDIR\n Path: /\n - Chmod: \"\"\n Chown: \"\"\n DestPath: ./\n From: \"\"\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - .\n - Args:\n - Comment: \"\"\n Key: SIGNER_BINARY_LINK\n Value: '\"https://d2hvyiie56hcat.cloudfront.net/linux/amd64/plugin/latest/notation-aws-signer-plugin.zip\"'\n Name: ARG\n - Args:\n - Comment: \"\"\n Key: SIGNER_BINARY_FILE\n Value: '\"notation-aws-signer-plugin.zip\"'\n Name: ARG\n - CmdLine:\n - wget -O ${SIGNER_BINARY_FILE} ${SIGNER_BINARY_LINK}\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n - CmdLine:\n - apk update \u0026\u0026 apk add unzip \u0026\u0026 unzip -o ${SIGNER_BINARY_FILE}\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n - CmdLine:\n - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=\"-w -s\" -o kyverno-notation-aws\n .\n Files: null\n FlagsUsed: []\n Name: RUN\n PrependShell: true\n Comment: \"\"\n From:\n Image: '\"golang:1.20.6-alpine3.18\"'\n Location:\n - End:\n Character: 0\n Line: 4\n Start:\n Character: 0\n Line: 4\n Name: builder\n Platform: $BUILD_PLATFORM\n SourceCode: FROM --platform=$BUILD_PLATFORM $BUILDER_IMAGE as builder\n- BaseName: gcr.io/distroless/static:nonroot\n Commands:\n - Name: WORKDIR\n Path: /\n - Env:\n - Key: PLUGINS_DIR\n Value: /plugins\n Name: ENV\n - Chmod: \"\"\n Chown: \"\"\n DestPath: plugins/com.amazonaws.signer.notation.plugin/notation-com.amazonaws.signer.notation.plugin\n From: builder\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - notation-com.amazonaws.signer.notation.plugin\n - Chmod: \"\"\n Chown: \"\"\n DestPath: kyverno-notation-aws\n From: builder\n Link: false\n Name: COPY\n SourceContents: null\n SourcePaths:\n - kyverno-notation-aws\n - CmdLine:\n - /kyverno-notation-aws\n Files: null\n Name: ENTRYPOINT\n PrependShell: false\n Comment: \"\"\n From:\n Image: gcr.io/distroless/static:nonroot\n Location:\n - End:\n Character: 0\n Line: 20\n Start:\n Character: 0\n Line: 20\n Name: \"\"\n Platform: \"\"\n SourceCode: FROM gcr.io/distroless/static:nonroot\n" }, {