Skip to content

Commit 220f695

Browse files
authored
Rule Type schema validation: change library and apply defaults (#4953)
* Rule Type schema validation: change library and apply defaults This changes the underlying library to something that's actually maintained: github.com/santhosh-tekuri/jsonschema/v6 It also adds the feature of applying defaults if they were defined in the JSON schema. Signed-off-by: Juan Antonio Osorio <[email protected]> * Hook rule validation into engine so defaults are set. Signed-off-by: Juan Antonio Osorio <[email protected]> * Address comments Signed-off-by: Juan Antonio Osorio <[email protected]> --------- Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent 9b7eaac commit 220f695

File tree

5 files changed

+148
-29
lines changed

5 files changed

+148
-29
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ require (
6060
github.com/puzpuzpuz/xsync/v3 v3.4.0
6161
github.com/robfig/cron/v3 v3.0.1
6262
github.com/rs/zerolog v1.33.0
63+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
6364
github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql v1.22.0
6465
github.com/signalfx/splunk-otel-go/instrumentation/github.com/lib/pq/splunkpq v1.22.0
6566
github.com/sigstore/protobuf-specs v0.3.2
@@ -74,7 +75,6 @@ require (
7475
github.com/styrainc/regal v0.29.1
7576
github.com/thomaspoignant/go-feature-flag v1.38.0
7677
github.com/xanzy/go-gitlab v0.113.0
77-
github.com/xeipuuv/gojsonschema v1.2.0
7878
github.com/yuin/goldmark v1.7.8
7979
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0
8080
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0

go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR
948948
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
949949
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
950950
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
951+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
952+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
951953
github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
952954
github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
953955
github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
@@ -1074,7 +1076,6 @@ github.com/xanzy/go-gitlab v0.113.0 h1:v5O4R+YZbJGxKqa9iIZxjMyeKkMKBN8P6sZsNl+Yc
10741076
github.com/xanzy/go-gitlab v0.113.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
10751077
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
10761078
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
1077-
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
10781079
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
10791080
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
10801081
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=

pkg/engine/v1/rtengine/engine.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,22 @@ func (r *RuleTypeEngine) Eval(
146146
}
147147
}()
148148

149+
// The rule type has already been validated at creation time. However,
150+
// re-validating it here is a good idea to ensure that the rule type
151+
// has not been tampered with. Also, this sets the defaults for the
152+
// rule definition.
153+
if ruleDef != nil {
154+
if err := r.ruleValidator.ValidateRuleDefAgainstSchema(ruleDef); err != nil {
155+
return fmt.Errorf("rule definition validation failed: %w", err)
156+
}
157+
}
158+
159+
if ruleParams != nil {
160+
if err := r.ruleValidator.ValidateParamsAgainstSchema(ruleParams); err != nil {
161+
return fmt.Errorf("rule parameters validation failed: %w", err)
162+
}
163+
}
164+
149165
logger.Info().Msg("entity evaluation - ingest started")
150166
// Try looking at the ingesting cache first
151167
result, ok := r.ingestCache.Get(r.ingester, entity, ruleParams)

pkg/profiles/rule_validator.go

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
"fmt"
88
"strings"
99

10-
"github.com/xeipuuv/gojsonschema"
10+
"github.com/santhosh-tekuri/jsonschema/v6"
11+
"google.golang.org/protobuf/types/known/structpb"
1112

1213
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
1314
)
@@ -18,32 +19,32 @@ type RuleValidator struct {
1819
ruleTypeName string
1920

2021
// schema is the schema that this rule type must conform to
21-
schema *gojsonschema.Schema
22+
schema *jsonschema.Schema
2223
// paramSchema is the schema that the parameters for this rule type must conform to
23-
paramSchema *gojsonschema.Schema
24+
paramSchema *jsonschema.Schema
2425
}
2526

2627
// NewRuleValidator creates a new rule validator
2728
func NewRuleValidator(rt *minderv1.RuleType) (*RuleValidator, error) {
28-
// Load schemas
29-
schemaLoader := gojsonschema.NewGoLoader(rt.Def.RuleSchema)
30-
schema, err := gojsonschema.NewSchema(schemaLoader)
29+
if rt.GetDef().GetRuleSchema() == nil {
30+
return nil, fmt.Errorf("rule type %s does not have a rule schema", rt.Name)
31+
}
32+
// Create a new schema compiler
33+
// Compile the main rule schema
34+
mainSchema, err := compileSchemaFromPB(rt.GetDef().GetRuleSchema())
3135
if err != nil {
3236
return nil, fmt.Errorf("cannot create json schema: %w", err)
3337
}
3438

35-
var paramSchema *gojsonschema.Schema
36-
if rt.Def.ParamSchema != nil {
37-
paramSchemaLoader := gojsonschema.NewGoLoader(rt.Def.ParamSchema)
38-
paramSchema, err = gojsonschema.NewSchema(paramSchemaLoader)
39-
if err != nil {
40-
return nil, fmt.Errorf("cannot create json schema for params: %w", err)
41-
}
39+
// Compile the parameter schema if it exists
40+
paramSchema, err := compileSchemaFromPB(rt.GetDef().GetParamSchema())
41+
if err != nil {
42+
return nil, fmt.Errorf("cannot create json schema for params: %w", err)
4243
}
4344

4445
return &RuleValidator{
4546
ruleTypeName: rt.Name,
46-
schema: schema,
47+
schema: mainSchema,
4748
paramSchema: paramSchema,
4849
}, nil
4950
}
@@ -57,7 +58,7 @@ func (r *RuleValidator) ValidateRuleDefAgainstSchema(contextualProfile map[strin
5758
Err: err.Error(),
5859
}
5960
}
60-
61+
applyDefaults(r.schema, contextualProfile)
6162
return nil
6263
}
6364

@@ -67,36 +68,66 @@ func (r *RuleValidator) ValidateParamsAgainstSchema(params map[string]any) error
6768
if r.paramSchema == nil {
6869
return nil
6970
}
70-
7171
if err := validateAgainstSchema(r.paramSchema, params); err != nil {
7272
return &RuleValidationError{
7373
RuleType: r.ruleTypeName,
7474
Err: err.Error(),
7575
}
7676
}
77-
77+
applyDefaults(r.paramSchema, params)
7878
return nil
7979
}
8080

81-
func validateAgainstSchema(schema *gojsonschema.Schema, obj map[string]any) error {
82-
documentLoader := gojsonschema.NewGoLoader(obj)
83-
result, err := schema.Validate(documentLoader)
84-
if err != nil {
85-
return fmt.Errorf("cannot validate json schema: %s", err)
81+
func compileSchemaFromPB(schemaData *structpb.Struct) (*jsonschema.Schema, error) {
82+
if schemaData == nil {
83+
return nil, nil
8684
}
8785

88-
if !result.Valid() {
89-
return buildValidationError(result.Errors())
86+
return compileSchemaFromMap(schemaData.AsMap())
87+
}
88+
89+
func compileSchemaFromMap(schemaData map[string]any) (*jsonschema.Schema, error) {
90+
compiler := jsonschema.NewCompiler()
91+
if err := compiler.AddResource("schema.json", schemaData); err != nil {
92+
return nil, fmt.Errorf("invalid schema: %w", err)
9093
}
94+
return compiler.Compile("schema.json")
95+
}
9196

97+
func validateAgainstSchema(schema *jsonschema.Schema, obj map[string]any) error {
98+
if err := schema.Validate(obj); err != nil {
99+
if verror, ok := err.(*jsonschema.ValidationError); ok {
100+
return buildValidationError(verror.Causes)
101+
}
102+
return fmt.Errorf("invalid json schema: %s", err)
103+
}
92104
return nil
93105
}
94106

95-
func buildValidationError(errs []gojsonschema.ResultError) error {
107+
func buildValidationError(errs []*jsonschema.ValidationError) error {
96108
problems := make([]string, 0, len(errs))
97109
for _, desc := range errs {
98-
problems = append(problems, desc.String())
110+
problems = append(problems, desc.Error())
99111
}
100-
101112
return fmt.Errorf("invalid json schema: %s", strings.TrimSpace(strings.Join(problems, "\n")))
102113
}
114+
115+
// applyDefaults recursively applies default values from the schema to the object.
116+
func applyDefaults(schema *jsonschema.Schema, obj map[string]any) {
117+
for key, def := range schema.Properties {
118+
// If the key does not exist in obj, apply the default value from the schema if present
119+
if _, exists := obj[key]; !exists && def.Default != nil {
120+
obj[key] = *def.Default
121+
}
122+
123+
// If def has properties, apply defaults to the nested object
124+
if def.Properties != nil {
125+
o, ok := obj[key].(map[string]any)
126+
if !ok {
127+
// cannot apply defaults to non-object types
128+
continue
129+
}
130+
applyDefaults(def, o)
131+
}
132+
}
133+
}

pkg/profiles/rule_validator_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,77 @@ import (
1515
"github.com/mindersec/minder/pkg/profiles"
1616
)
1717

18+
func TestSetDefaultValuesOnValidation(t *testing.T) {
19+
t.Parallel()
20+
21+
rtstr := `
22+
---
23+
version: v1
24+
release_phase: alpha
25+
type: rule-type
26+
name: foo
27+
display_name: Foo
28+
short_failure_message: Foo failed
29+
severity:
30+
value: medium
31+
context:
32+
provider: github
33+
description: Very important rule
34+
guidance: |
35+
This is how you should do it.
36+
def:
37+
in_entity: repository
38+
rule_schema:
39+
type: object
40+
properties:
41+
schedule_interval:
42+
type: string
43+
description: |
44+
Sets the schedule interval in cron format for the workflow. Only applicable for remediation.
45+
publish_results:
46+
type: boolean
47+
description: |
48+
Publish the results of the analysis.
49+
default: true
50+
retention_days:
51+
type: integer
52+
description: |
53+
Number of days to retain the SARIF file.
54+
default: 5
55+
sarif_file:
56+
type: string
57+
description: |
58+
Name of the SARIF file.
59+
default: "results.sarif"
60+
required:
61+
- schedule_interval
62+
- publish_results
63+
`
64+
65+
rt := &minderv1.RuleType{}
66+
require.NoError(t, minderv1.ParseResource(strings.NewReader(rtstr), rt), "failed to parse rule type")
67+
68+
rval, err := profiles.NewRuleValidator(rt)
69+
require.NoError(t, err, "failed to create rule validator")
70+
71+
obj := map[string]any{
72+
"schedule_interval": "0 0 * * *",
73+
"publish_results": false,
74+
"retention_days": 10,
75+
}
76+
77+
// Validation should pass
78+
require.NoError(t, rval.ValidateRuleDefAgainstSchema(obj), "failed to validate rule definition")
79+
80+
// Value is left as is
81+
require.Equal(t, "0 0 * * *", obj["schedule_interval"])
82+
require.Equal(t, 10, obj["retention_days"])
83+
require.Equal(t, false, obj["publish_results"])
84+
85+
// default is set
86+
require.Equal(t, "results.sarif", obj["sarif_file"])
87+
}
88+
1889
func TestExampleRulesAreValidatedCorrectly(t *testing.T) {
1990
t.Parallel()
2091

0 commit comments

Comments
 (0)