From d8947db27d5feeeaf87d1be18fa597814766ed4e Mon Sep 17 00:00:00 2001 From: Jasper Van der Jeugt Date: Fri, 23 Oct 2020 15:53:55 +0200 Subject: [PATCH] topdown: add yaml.is_valid and json.is_valid Signed-off-by: Jasper Van der Jeugt --- ast/builtins.go | 20 ++++++ capabilities.json | 28 ++++++++ docs/content/policy-reference.md | 2 + .../testdata/jsonbuiltins/test-is-valid.yaml | 68 +++++++++++++++++++ topdown/encoding.go | 25 +++++++ 5 files changed, 143 insertions(+) create mode 100644 test/cases/testdata/jsonbuiltins/test-is-valid.yaml diff --git a/ast/builtins.go b/ast/builtins.go index 2c91456c25..eced166854 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -128,6 +128,7 @@ var DefaultBuiltins = [...]*Builtin{ // Encoding JSONMarshal, JSONUnmarshal, + JSONIsValid, Base64Encode, Base64Decode, Base64IsValid, @@ -139,6 +140,7 @@ var DefaultBuiltins = [...]*Builtin{ URLQueryDecodeObject, YAMLMarshal, YAMLUnmarshal, + YAMLIsValid, // Object Manipulation ObjectUnion, @@ -1075,6 +1077,15 @@ var JSONUnmarshal = &Builtin{ ), } +// JSONIsValid verifies the input string is a valid JSON document. +var JSONIsValid = &Builtin{ + Name: "json.is_valid", + Decl: types.NewFunction( + types.Args(types.S), + types.B, + ), +} + // JSONFilter filters the JSON object var JSONFilter = &Builtin{ Name: "json.filter", @@ -1324,6 +1335,15 @@ var YAMLUnmarshal = &Builtin{ ), } +// YAMLIsValid verifies the input string is a valid YAML document. +var YAMLIsValid = &Builtin{ + Name: "yaml.is_valid", + Decl: types.NewFunction( + types.Args(types.S), + types.B, + ), +} + /** * Tokens */ diff --git a/capabilities.json b/capabilities.json index ccca1210e2..9dc7c080c3 100644 --- a/capabilities.json +++ b/capabilities.json @@ -1420,6 +1420,20 @@ "type": "function" } }, + { + "name": "json.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, { "name": "json.marshal", "decl": { @@ -3211,6 +3225,20 @@ }, "relation": true }, + { + "name": "yaml.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, { "name": "yaml.marshal", "decl": { diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index acfc2e612a..8854db545a 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -450,8 +450,10 @@ The following table shows examples of how ``glob.match`` works: | ``output := urlquery.decode_object(string)`` | ``output`` is ``object`` deserialized from a URL query parameter string | | ``output := json.marshal(x)`` | ``output`` is ``x`` serialized to a JSON string | | ``output := json.unmarshal(string)`` | ``output`` is ``string`` deserialized to a term from a JSON encoded string | +| ``output := json.is_valid(string)`` | ``output`` is a ``boolean`` that indicated whether ``string`` is a valid JSON document | | ``output := yaml.marshal(x)`` | ``output`` is ``x`` serialized to a YAML string | | ``output := yaml.unmarshal(string)`` | ``output`` is ``string`` deserialized to a term from YAML encoded string | +| ``output := yaml.is_valid(string)`` | ``output`` is a ``boolean`` that indicated whether ``string`` is a valid YAML document that can be decoded by `yaml.unmarshal` | ### Token Signing diff --git a/test/cases/testdata/jsonbuiltins/test-is-valid.yaml b/test/cases/testdata/jsonbuiltins/test-is-valid.yaml new file mode 100644 index 0000000000..b2915f2f57 --- /dev/null +++ b/test/cases/testdata/jsonbuiltins/test-is-valid.yaml @@ -0,0 +1,68 @@ +cases: +- note: jsonbuiltins/json is_valid + query: data.generated.p = x + modules: + - | + package generated + + documents = [ + `plainstring`, + `{`, + `{"json": "ok"}`, + ] + + p = [x | doc = documents[_]; json.is_valid(doc, x)] + want_result: + - x: + - false + - false + - true + +- note: jsonbuiltins/json is_valid not string + modules: + - | + package generated + + p = x { + json.is_valid(input.foo, x) + } + query: data.generated.p = x + input: {"foo": 1} + want_error: operand 1 must be string but got number + want_error_code: eval_type_error + +- note: jsonbuiltins/yaml is_valid + query: data.generated.p = x + modules: + - | + package generated + + documents = [ + `foo: + - qux: bar + - baz: 2`, + `foo: + - qux: bar + - baz: {`, + `{"json": "ok"}`, + ] + + p = [x | doc = documents[_]; yaml.is_valid(doc, x)] + want_result: + - x: + - true + - false + - true + +- note: jsonbuiltins/yaml is_valid not string + modules: + - | + package generated + + p = x { + yaml.is_valid(input.foo, x) + } + query: data.generated.p = x + input: {"foo": 1} + want_error: operand 1 must be string but got number + want_error_code: eval_type_error diff --git a/topdown/encoding.go b/topdown/encoding.go index 09556d8133..545b9d3827 100644 --- a/topdown/encoding.go +++ b/topdown/encoding.go @@ -50,6 +50,18 @@ func builtinJSONUnmarshal(a ast.Value) (ast.Value, error) { return ast.InterfaceToValue(x) } +func builtinJSONIsValid(a ast.Value) (ast.Value, error) { + + str, err := builtins.StringOperand(a, 1) + if err != nil { + return nil, err + } + + var x interface{} + err = util.UnmarshalJSON([]byte(str), &x) + return ast.Boolean(err == nil), nil +} + func builtinBase64Encode(a ast.Value) (ast.Value, error) { str, err := builtins.StringOperand(a, 1) if err != nil { @@ -235,9 +247,21 @@ func builtinYAMLUnmarshal(a ast.Value) (ast.Value, error) { return ast.InterfaceToValue(val) } +func builtinYAMLIsValid(a ast.Value) (ast.Value, error) { + str, err := builtins.StringOperand(a, 1) + if err != nil { + return nil, err + } + + var x interface{} + err = ghodss.Unmarshal([]byte(str), &x) + return ast.Boolean(err == nil), nil +} + func init() { RegisterFunctionalBuiltin1(ast.JSONMarshal.Name, builtinJSONMarshal) RegisterFunctionalBuiltin1(ast.JSONUnmarshal.Name, builtinJSONUnmarshal) + RegisterFunctionalBuiltin1(ast.JSONIsValid.Name, builtinJSONIsValid) RegisterFunctionalBuiltin1(ast.Base64Encode.Name, builtinBase64Encode) RegisterFunctionalBuiltin1(ast.Base64Decode.Name, builtinBase64Decode) RegisterFunctionalBuiltin1(ast.Base64IsValid.Name, builtinBase64IsValid) @@ -249,4 +273,5 @@ func init() { RegisterBuiltinFunc(ast.URLQueryDecodeObject.Name, builtinURLQueryDecodeObject) RegisterFunctionalBuiltin1(ast.YAMLMarshal.Name, builtinYAMLMarshal) RegisterFunctionalBuiltin1(ast.YAMLUnmarshal.Name, builtinYAMLUnmarshal) + RegisterFunctionalBuiltin1(ast.YAMLIsValid.Name, builtinYAMLIsValid) }