diff --git a/examples/CEL/policy.yaml b/examples/CEL/policy.yaml new file mode 100755 index 0000000000..7a418fbde5 --- /dev/null +++ b/examples/CEL/policy.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +policies: + - name: CEL_policy + isDefault: true + rules: + - identifier: CUSTOM_DEPLOYMENT_BILLING_LABEL_EXISTS + messageOnFailure: "workloads labels should contain billing label" + - identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS + messageOnFailure: "secret labels should contain environment label" + + +customRules: + - identifier: CUSTOM_WORKLOADS_BILLING_LABEL_EXISTS + name: Ensure Workloads has billing label [CUSTOM RULE] + defaultMessageOnFailure: workloads labels should contain billing label + schema: + # constraint schema + if: + properties: + kind: + type: string + enum: + - Deployment + - Pod + then: + CELDefinition: + - expression: "object.kind != 'Deployment' || (has(object.metadata.labels) && has(object.metadata.labels.billing))" + message: "deployment labels should contain billing label" + - expression: "object.kind != 'Pod' || (has(object.metadata.labels) && has(object.metadata.labels.billing))" + message: "pod labels should contain billing label" + - identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS + name: Ensure Secret has environment label [CUSTOM RULE] + defaultMessageOnFailure: secret labels should contain environment label + schema: + # constraint schema + if: + properties: + kind: + type: string + enum: + - Secret + then: + CELDefinition: + - expression: "has(object.metadata.labels) && has(object.metadata.labels.environment)" + diff --git a/go.mod b/go.mod index bd340f50d3..6480d7ff42 100644 --- a/go.mod +++ b/go.mod @@ -29,13 +29,17 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/open-policy-agent/opa v0.49.2 +require ( + github.com/google/cel-go v0.16.0 + github.com/open-policy-agent/opa v0.49.2 +) require ( github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/a8m/envsubst v1.3.0 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/alecthomas/participle/v2 v2.0.0-beta.5 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.9.11 // indirect @@ -46,10 +50,15 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect - golang.org/x/net v0.8.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.10.0 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect ) @@ -78,8 +87,8 @@ require ( github.com/tklauser/numcpus v0.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.51.0 // indirect ) diff --git a/go.sum b/go.sum index 40beeab75f..567844ec16 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0q github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= @@ -168,10 +170,13 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.16.0 h1:DG9YQ8nFCFXAs/FDDwBxmL1tpKNrdlGUM9U3537bX/Y= +github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -182,6 +187,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -380,6 +386,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -454,6 +462,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -512,8 +522,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -583,8 +593,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -593,8 +603,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -718,6 +728,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -744,7 +758,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/jsonSchemaValidator/extensions/customKeyCELDefinition.go b/pkg/jsonSchemaValidator/extensions/customKeyCELDefinition.go new file mode 100644 index 0000000000..8cba1de652 --- /dev/null +++ b/pkg/jsonSchemaValidator/extensions/customKeyCELDefinition.go @@ -0,0 +1,132 @@ +// This file defines a custom key to implement the logic for cel rule: + +package jsonSchemaValidator + +import ( + "encoding/json" + "fmt" + + "github.com/google/cel-go/cel" + "github.com/santhosh-tekuri/jsonschema/v5" +) + +const CELDefinitionCustomKey = "CELDefinition" + +type CustomKeyCELDefinitionCompiler struct{} + +type CustomKeyCELDefinitionSchema []interface{} + +var CustomKeyCELRule = jsonschema.MustCompileString("customKeyCELDefinition.json", `{ + "properties" : { + "CELDefinition": { + "type": "array" + } + } +}`) + +func (CustomKeyCELDefinitionCompiler) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (jsonschema.ExtSchema, error) { + if customKeyCELRule, ok := m[CELDefinitionCustomKey]; ok { + customKeyCELRuleObj, validObject := customKeyCELRule.([]interface{}) + if !validObject { + return nil, fmt.Errorf("CELDefinition must be an array") + } + + CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELRuleObj) + if err != nil { + return nil, err + } + + if len(CELDefinitionSchema.CELExpressions) == 0 { + return nil, fmt.Errorf("CELDefinition can't be empty") + } + + return CustomKeyCELDefinitionSchema(customKeyCELRuleObj), nil + } + return nil, nil +} + +func (customKeyCELDefinitionSchema CustomKeyCELDefinitionSchema) Validate(ctx jsonschema.ValidationContext, dataValue interface{}) error { + CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELDefinitionSchema) + if err != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) + } + // wrap dataValue (the resource that should be validated) inside a struct with parent object key + resourceWithParentKey := make(map[string]interface{}) + resourceWithParentKey["object"] = dataValue + + // prepare CEL env inputs - in our case the only input is the resource that should be validated + inputs, err := getCELEnvInputs(resourceWithParentKey) + if err != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) + } + + env, err := cel.NewEnv(inputs...) + if err != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error()) + } + + for _, celExpression := range CELDefinitionSchema.CELExpressions { + ast, issues := env.Compile(celExpression.Expression) + if issues != nil && issues.Err() != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression compile error: %s", issues.Err()) + } + + prg, err := env.Program(ast) + if err != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, "cel program construction error: %s", err) + } + + res1, _, err := prg.Eval(resourceWithParentKey) + if err != nil { + return ctx.Error(CustomKeyValidationErrorKeyPath, "cel evaluation error: %s", err) + } + + if res1.Type().TypeName() != "bool" { + return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean") + } + + celReturnValue, ok := res1.Value().(bool) + if !ok { + return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean") + } + if !celReturnValue { + return ctx.Error(CELDefinitionCustomKey, "values in data value %v do not match", dataValue) + } + } + + return nil +} + +type CELExpression struct { + Expression string `json:"expression"` +} + +type CELDefinition struct { + CELExpressions []CELExpression +} + +func convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(CELDefinitionSchema CustomKeyCELDefinitionSchema) (*CELDefinition, error) { + var CELDefinition CELDefinition + for _, CELExpressionFromSchema := range CELDefinitionSchema { + var CELExpression CELExpression + b, err := json.Marshal(CELExpressionFromSchema) + if err != nil { + return nil, fmt.Errorf("CELExpression failed to marshal to json, %s", err.Error()) + } + err = json.Unmarshal(b, &CELExpression) + if err != nil { + return nil, fmt.Errorf("CELExpression must be an object of type CELExpression %s", err.Error()) + } + CELDefinition.CELExpressions = append(CELDefinition.CELExpressions, CELExpression) + } + + return &CELDefinition, nil +} + +func getCELEnvInputs(dataValue map[string]interface{}) ([]cel.EnvOption, error) { + inputVars := make([]cel.EnvOption, 0, len(dataValue)) + for input := range dataValue { + inputVars = append(inputVars, cel.Variable(input, cel.DynType)) + } + return inputVars, nil +} diff --git a/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition-expression.json b/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition-expression.json new file mode 100644 index 0000000000..ae18470488 --- /dev/null +++ b/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition-expression.json @@ -0,0 +1,8 @@ +{ + "CELDefinition": [ + { + "expression": "hassss(object.metadata.labels) && has(object.metadata.labels.billing)", + "message": "deployment labels should contain billing label" + } + ] +} diff --git a/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition.json b/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition.json new file mode 100644 index 0000000000..7c054c1d40 --- /dev/null +++ b/pkg/jsonSchemaValidator/test_fixtures/invalid-cel-definition.json @@ -0,0 +1,8 @@ +{ + "CELDefinition": [ + { + "expression": 1, + "message": "deployment labels should contain billing label" + } + ] +} diff --git a/pkg/jsonSchemaValidator/test_fixtures/rego-rule-fail.yaml b/pkg/jsonSchemaValidator/test_fixtures/rule-fail.yaml similarity index 100% rename from pkg/jsonSchemaValidator/test_fixtures/rego-rule-fail.yaml rename to pkg/jsonSchemaValidator/test_fixtures/rule-fail.yaml diff --git a/pkg/jsonSchemaValidator/test_fixtures/rego-rule-pass.yaml b/pkg/jsonSchemaValidator/test_fixtures/rule-pass.yaml similarity index 100% rename from pkg/jsonSchemaValidator/test_fixtures/rego-rule-pass.yaml rename to pkg/jsonSchemaValidator/test_fixtures/rule-pass.yaml diff --git a/pkg/jsonSchemaValidator/test_fixtures/valid-cel-definition.json b/pkg/jsonSchemaValidator/test_fixtures/valid-cel-definition.json new file mode 100644 index 0000000000..ff58d06004 --- /dev/null +++ b/pkg/jsonSchemaValidator/test_fixtures/valid-cel-definition.json @@ -0,0 +1,8 @@ +{ + "CELDefinition": [ + { + "expression": "has(object.metadata.labels) && has(object.metadata.labels.billing)", + "message": "deployment labels should contain billing label" + } + ] +} diff --git a/pkg/jsonSchemaValidator/validator.go b/pkg/jsonSchemaValidator/validator.go index ac5f65462d..6d5b83940b 100644 --- a/pkg/jsonSchemaValidator/validator.go +++ b/pkg/jsonSchemaValidator/validator.go @@ -73,6 +73,7 @@ func (jsv *JSONSchemaValidator) Validate(schemaContent string, yamlContent []byt compiler.RegisterExtension("customKeyRule89", extensions.CustomKeyRule89, extensions.CustomKeyRule89Compiler{}) compiler.RegisterExtension("customKeyRule101", extensions.CustomKeyRule101, extensions.CustomKeyRule101Compiler{}) compiler.RegisterExtension("customKeyRegoRule", extensions.CustomKeyRegoRule, extensions.CustomKeyRegoDefinitionCompiler{}) + compiler.RegisterExtension("customKeyCELRule", extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{}) // compiler.Compile() is an expensive operation. We cache the compiled schema in rulesSchemasCache to avoid re-compiling the same schema. schemaAny, ok := jsv.rulesSchemasCache.Load(schemaContent) diff --git a/pkg/jsonSchemaValidator/validator_test.go b/pkg/jsonSchemaValidator/validator_test.go index 9569fad3ee..c93b6958f0 100644 --- a/pkg/jsonSchemaValidator/validator_test.go +++ b/pkg/jsonSchemaValidator/validator_test.go @@ -3,6 +3,7 @@ package jsonSchemaValidator import ( _ "embed" "encoding/json" + "fmt" "strings" "testing" @@ -49,14 +50,20 @@ func TestValidateResourceMinMaxCustomKeysPass(t *testing.T) { //go:embed test_fixtures/invalid-rego-definition.json var invalidRegoDefinitionJson string +//go:embed test_fixtures/invalid-cel-definition.json +var invalidCELDefinitionJson string + //go:embed test_fixtures/valid-rego-definition.json var validRegoDefinitionJson string -//go:embed test_fixtures/rego-rule-fail.yaml -var regoRuleFail string +//go:embed test_fixtures/valid-cel-definition.json +var validCELDefinitionJson string + +//go:embed test_fixtures/rule-fail.yaml +var ruleFail string -//go:embed test_fixtures/rego-rule-pass.yaml -var regoRulePass string +//go:embed test_fixtures/rule-pass.yaml +var rulePass string func TestRegoDefinitionCustomKey(t *testing.T) { t.Run("invalidSchema", func(t *testing.T) { @@ -83,7 +90,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) { t.Fatal(err) } t.Run("validInstance", func(t *testing.T) { - jsonYamlContent, err := getInterfaceFromYamlContext(regoRulePass) + jsonYamlContent, err := getInterfaceFromYamlContext(rulePass) if err != nil { t.Fatal(err) } @@ -93,7 +100,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) { } }) t.Run("invalidInstance", func(t *testing.T) { - jsonYamlContent, err := getInterfaceFromYamlContext(regoRuleFail) + jsonYamlContent, err := getInterfaceFromYamlContext(ruleFail) if err != nil { t.Fatal(err) } @@ -112,7 +119,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) { func TestValidateRegoDefinitionCustomKeyPass(t *testing.T) { passResourceYamlFileContent, customRuleSchemaYamlFileContent := getResourceAndSchemaYamlContentsAsString( - regoYamlFilesPath+"/rego-rule-pass.yaml", + regoYamlFilesPath+"/rule-pass.yaml", regoYamlFilesPath+"/valid-rego-definition.json", ) @@ -140,7 +147,7 @@ func TestValidateRegoDefinitionCustomKeyPassDueToResourceNotInConstraint(t *test func TestValidateRegoDefinitionCustomKeyFail(t *testing.T) { failResourceYamlFileContent, customRuleSchemaYamlFileContent := getResourceAndSchemaYamlContentsAsString( - regoYamlFilesPath+"/rego-rule-fail.yaml", + regoYamlFilesPath+"/rule-fail.yaml", regoYamlFilesPath+"/valid-rego-definition.json", ) @@ -155,7 +162,7 @@ func TestValidateRegoDefinitionCustomKeyFail(t *testing.T) { func TestValidateRegoDefinitionCustomKeyFailDueToRegoCompile(t *testing.T) { failResourceYamlFileContent, customRuleSchemaYamlFileContent := getResourceAndSchemaYamlContentsAsString( - regoYamlFilesPath+"/rego-rule-pass.yaml", + regoYamlFilesPath+"/rule-pass.yaml", regoYamlFilesPath+"/invalid-rego-definition-code.json", ) @@ -167,6 +174,87 @@ func TestValidateRegoDefinitionCustomKeyFailDueToRegoCompile(t *testing.T) { assert.Contains(t, errorsResult[0].Error, "can't compile rego code, rego code must have a package") } +func TestCELDefinitionCustomKey(t *testing.T) { + t.Run("invalidSchema", func(t *testing.T) { + c := jsonschema.NewCompiler() + c.RegisterExtension(extensions.CELDefinitionCustomKey, extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{}) + err := c.AddResource("test.json", strings.NewReader(invalidCELDefinitionJson)) + if err != nil { + t.Fatal(err) + } + _, err = c.Compile("test.json") + if err == nil { + t.Fatal("error expected") + } + assert.Contains(t, err.Error(), "CELExpression must be an object of type CELExpression json: cannot unmarshal number into Go struct field CELExpression.expression of type string") + }) + t.Run("validSchema", func(t *testing.T) { + c := jsonschema.NewCompiler() + c.RegisterExtension(extensions.CELDefinitionCustomKey, extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{}) + if err := c.AddResource("test.json", strings.NewReader(validCELDefinitionJson)); err != nil { + t.Fatal(err) + } + schema, err := c.Compile("test.json") + if err != nil { + t.Fatal(err) + } + t.Run("validInstance", func(t *testing.T) { + jsonYamlContent, err := getInterfaceFromYamlContext(rulePass) + if err != nil { + t.Fatal(err) + } + + if err := schema.Validate(jsonYamlContent); err != nil { + t.Fatal(err) + } + }) + t.Run("invalidInstance", func(t *testing.T) { + jsonYamlContent, err := getInterfaceFromYamlContext(ruleFail) + if err != nil { + t.Fatal(err) + } + if err := schema.Validate(jsonYamlContent); err == nil { + t.Fatal("validation must fail") + } else { + fmt.Println(err.(*jsonschema.ValidationError).GoString()) + t.Logf("%#v", err) + if !strings.Contains(err.(*jsonschema.ValidationError).GoString(), "doesn't validate") { + t.Fatal("validation error expected to contain CELDefinition message") + } + } + }) + }) +} + +func TestValidateCELDefinitionCustomKeyPass(t *testing.T) { + passResourceYamlFileContent, customRuleSchemaYamlFileContent := + getResourceAndSchemaYamlContentsAsString( + regoYamlFilesPath+"/rule-pass.yaml", + regoYamlFilesPath+"/valid-cel-definition.json", + ) + + jsonSchemaValidator := New() + + errorsResult, _ := jsonSchemaValidator.ValidateYamlSchema(customRuleSchemaYamlFileContent, passResourceYamlFileContent) + + assert.Empty(t, errorsResult) +} + +func TestValidateCELDefinitionCustomKeyFailDueToCELCompile(t *testing.T) { + failResourceYamlFileContent, customRuleSchemaYamlFileContent := + getResourceAndSchemaYamlContentsAsString( + regoYamlFilesPath+"/rule-pass.yaml", + regoYamlFilesPath+"/invalid-cel-definition-expression.json", + ) + + jsonSchemaValidator := New() + + errorsResult, _ := jsonSchemaValidator.ValidateYamlSchema(customRuleSchemaYamlFileContent, failResourceYamlFileContent) + + assert.GreaterOrEqual(t, len(errorsResult), 1) + assert.Contains(t, errorsResult[0].Error, "cel expression compile error") +} + func getResourceAndSchemaYamlContentsAsString(resourceToValidatePath string, schemaPath string) (string, string) { fileReader := fileReader.CreateFileReader(nil) @@ -185,8 +273,8 @@ func getResourceAndSchemaYamlContentsAsString(resourceToValidatePath string, sch func getInterfaceFromYamlContext(yamlContent string) (interface{}, error) { var jsonYamlContent interface{} - regoRuleFailsYamlBytes, _ := yaml.YAMLToJSON([]byte(yamlContent)) - err := json.Unmarshal(regoRuleFailsYamlBytes, &jsonYamlContent) + ruleFailsYamlBytes, _ := yaml.YAMLToJSON([]byte(yamlContent)) + err := json.Unmarshal(ruleFailsYamlBytes, &jsonYamlContent) if err != nil { return nil, err }