diff --git a/bundle/regal/ast.rego b/bundle/regal/ast.rego index 4bd69e2d..39781bce 100644 --- a/bundle/regal/ast.rego +++ b/bundle/regal/ast.rego @@ -7,7 +7,21 @@ import future.keywords.in import data.regal.opa -_builtin_names := object.keys(opa.builtins) +builtin_names := object.keys(opa.builtins) + +# METADATA +# description: | +# provide the package name / path as originally declared in the +# input policy, so "package foo.bar" would return "foo.bar" +package_name := concat(".", [path.value | + some i, path in input["package"].path + i > 0 +]) + +tests := [rule | + some rule in input.rules + startswith(rule.head.name, "test_") +] # METADATA # description: parse provided snippet with a generic package declaration added @@ -40,7 +54,7 @@ _find_nested_vars(obj) := [value | # simple assignment, i.e. `x := 100` returns `x` # always returns a single var, but wrapped in an # array for consistency -_find_assign_vars(path, value) := var if { +_find_assign_vars(_, value) := var if { value[1].type == "var" var := [value[1]] } @@ -49,19 +63,19 @@ _find_assign_vars(path, value) := var if { # [a, b, c] := [1, 2, 3] # or # {a: b} := {"foo": "bar"} -_find_assign_vars(path, value) := var if { +_find_assign_vars(_, value) := var if { value[1].type in {"array", "object"} var := _find_nested_vars(value[1]) } # var declared via `some`, i.e. `some x` or `some x, y` -_find_some_decl_vars(path, value) := [v | +_find_some_decl_vars(_, value) := [v | some v in value v.type == "var" ] # single var declared via `some in`, i.e. `some x in y` -_find_some_in_decl_vars(path, value) := var if { +_find_some_in_decl_vars(_, value) := var if { arr := value[0].value count(arr) == 3 @@ -69,7 +83,7 @@ _find_some_in_decl_vars(path, value) := var if { } # two vars declared via `some in`, i.e. `some x, y in z` -_find_some_in_decl_vars(path, value) := var if { +_find_some_in_decl_vars(_, value) := var if { arr := value[0].value count(arr) == 4 @@ -81,7 +95,7 @@ _find_some_in_decl_vars(path, value) := var if { # one or two vars declared via `every`, i.e. `every x in y {}` # or `every`, i.e. `every x, y in y {}` -_find_every_vars(path, value) := var if { +_find_every_vars(_, value) := var if { key_var := [v | v := value.key; v.type == "var"; indexof(v.value, "$") == -1] val_var := [v | v := value.value; v.type == "var"; indexof(v.value, "$") == -1] @@ -133,7 +147,7 @@ find_builtin_calls(node) := [value | value[0].type == "ref" value[0].value[0].type == "var" - value[0].value[0].value in _builtin_names + value[0].value[0].value in builtin_names ] # METADATA diff --git a/bundle/regal/rules/bugs/bugs.rego b/bundle/regal/rules/bugs/bugs.rego deleted file mode 100644 index d3d8a284..00000000 --- a/bundle/regal/rules/bugs/bugs.rego +++ /dev/null @@ -1,197 +0,0 @@ -package regal.rules.bugs - -import future.keywords.contains -import future.keywords.if -import future.keywords.in - -import data.regal.ast -import data.regal.config -import data.regal.opa -import data.regal.result - -# We could probably include arrays and objects too, as a single compound value -# is not very useful, but it's not as clear cut as scalars, as you could have -# something like {"a": foo(input.x) == "bar"} which is not a constant condition, -# however meaningless it may be. Maybe consider for another rule? -_scalars := {"boolean", "null", "number", "string"} - -_operators := {"equal", "gt", "gte", "lt", "lte", "neq"} - -_rule_names := {name | name := input.rules[_].head.name} - -_rules_with_bodies := [rule | - some rule in input.rules - not probably_no_body(rule) -] - -# NOTE: The constant condition checks currently don't do nesting! -# Additionally, there are several other conditions that could be considered -# constant, or if not, redundant... so this rule should be expanded in time - -# METADATA -# title: constant-condition -# description: Constant condition -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/constant-condition -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in _rules_with_bodies - some expr in rule.body - - expr.terms.type in _scalars - - violation := result.fail(rego.metadata.rule(), result.location(expr)) -} - -# METADATA -# title: constant-condition -# description: Constant condition -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/constant-condition -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in _rules_with_bodies - some expr in rule.body - - expr.terms[0].value[0].type == "var" - expr.terms[0].value[0].value in _operators - - expr.terms[1].type in _scalars - expr.terms[2].type in _scalars - - violation := result.fail(rego.metadata.rule(), result.location(expr)) -} - -# METADATA -# title: top-level-iteration -# description: Iteration in top-level assignment -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/top-level-iteration -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - - rule.head.value.type == "ref" - - last := regal.last(rule.head.value.value) - last.type == "var" - - illegal_value_ref(last.value) - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} - -_builtin_names := object.keys(opa.builtins) - -# METADATA -# title: unused-return-value -# description: Non-boolean return value unused -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/unused-return-value -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - some expr in rule.body - - expr.terms[0].type == "ref" - expr.terms[0].value[0].type == "var" - - ref_name := expr.terms[0].value[0].value - ref_name in _builtin_names - - opa.builtins[ref_name].result.type != "boolean" - - # if return value is provided as last argument, it's not unused - # it's however another violation: function-arg-return - not ast.function_ret_in_args(ref_name, expr.terms) - - violation := result.fail(rego.metadata.rule(), result.location(expr.terms[0])) -} - -# METADATA -# title: not-equals-in-loop -# description: Use of != in loop -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/not-equals-in-loop -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - some expr in rule.body - - expr.terms[0].type == "ref" - expr.terms[0].value[0].type == "var" - expr.terms[0].value[0].value == "neq" - - some neq_term in array.slice(expr.terms, 1, count(expr.terms)) - neq_term.type == "ref" - - some i - neq_term.value[i].type == "var" - startswith(neq_term.value[i].value, "$") - - violation := result.fail(rego.metadata.rule(), result.location(expr.terms[0])) -} - -# regal ignore:external-reference -illegal_value_ref(value) if not value in _rule_names - -# i.e. allow {..}, or allow := true, which expands to allow = true { true } -probably_no_body(rule) if { - count(rule.body) == 1 - rule.body[0].terms.type == "boolean" - rule.body[0].terms.value == true -} - -# METADATA -# title: rule-shadows-builtin -# description: Rule name shadows built-in -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/rule-shadows-builtin -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule.head.name in _builtin_names - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} - -# METADATA -# title: rule-named-if -# description: Rule named "if" -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/rule-named-if -# custom: -# category: bugs -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule.head.name == "if" - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} diff --git a/bundle/regal/rules/bugs/bugs_test.rego b/bundle/regal/rules/bugs/bugs_test.rego deleted file mode 100644 index 2dddea9f..00000000 --- a/bundle/regal/rules/bugs/bugs_test.rego +++ /dev/null @@ -1,203 +0,0 @@ -package regal.rules.bugs_test - -import future.keywords.if - -import data.regal.ast -import data.regal.config -import data.regal.rules.bugs - -test_fail_simple_constant_condition if { - r := report(`allow { - 1 - }`) - r == {{ - "category": "bugs", - "description": "Constant condition", - "location": {"col": 2, "file": "policy.rego", "row": 9, "text": "\t1"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/constant-condition", "bugs"), - }], - "title": "constant-condition", - "level": "error", - }} -} - -test_success_static_condition_probably_generated if { - report(`allow { true }`) == set() -} - -test_fail_operator_constant_condition if { - r := report(`allow { - 1 == 1 - }`) - r == {{ - "category": "bugs", - "description": "Constant condition", - "location": {"col": 2, "file": "policy.rego", "row": 9, "text": "\t1 == 1"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/constant-condition", "bugs"), - }], - "title": "constant-condition", - "level": "error", - }} -} - -test_success_non_constant_condition if { - report(`allow { 1 == input.one }`) == set() -} - -test_fail_top_level_iteration_wildcard if { - r := report(`x := input.foo.bar[_]`) - r == {{ - "category": "bugs", - "description": "Iteration in top-level assignment", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "x := input.foo.bar[_]"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/top-level-iteration", "bugs"), - }], - "title": "top-level-iteration", - "level": "error", - }} -} - -test_fail_top_level_iteration_named_var if { - r := report(`x := input.foo.bar[i]`) - r == {{ - "category": "bugs", - "description": "Iteration in top-level assignment", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "x := input.foo.bar[i]"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/top-level-iteration", "bugs"), - }], - "title": "top-level-iteration", - "level": "error", - }} -} - -test_success_top_level_known_var_ref if { - report(` - i := "foo" - x := input.foo.bar[i]`) == set() -} - -test_success_top_level_input_ref if { - report(`x := input.foo.bar[input.y]`) == set() -} - -test_fail_unused_return_value if { - r := report(`allow { - indexof("s", "s") - }`) - r == {{ - "category": "bugs", - "description": "Non-boolean return value unused", - "level": "error", - "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tindexof(\"s\", \"s\")"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/unused-return-value", "bugs"), - }], - "title": "unused-return-value", - }} -} - -test_success_unused_boolean_return_value if { - report(`allow { startswith("s", "s") }`) == set() -} - -test_success_return_value_assigned if { - report(`allow { x := indexof("s", "s") }`) == set() -} - -test_fail_neq_in_loop if { - r := report(`deny { - "admin" != input.user.groups[_] - input.user.groups[_] != "admin" - }`) - r == { - { - "category": "bugs", - "description": "Use of != in loop", - "level": "error", - "location": {"col": 11, "file": "policy.rego", "row": 9, "text": "\t\t\"admin\" != input.user.groups[_]"}, - "related_resources": [{ - "description": "documentation", - "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", - }], - "title": "not-equals-in-loop", - }, - { - "category": "bugs", - "description": "Use of != in loop", - "level": "error", - "location": {"col": 24, "file": "policy.rego", "row": 10, "text": "\t\tinput.user.groups[_] != \"admin\""}, - "related_resources": [{ - "description": "documentation", - "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", - }], - "title": "not-equals-in-loop", - }, - } -} - -test_fail_neq_in_loop_one_liner if { - r := report(`deny if "admin" != input.user.groups[_]`) - r == {{ - "category": "bugs", - "description": "Use of != in loop", - "level": "error", - "location": {"col": 17, "file": "policy.rego", "row": 8, "text": "deny if \"admin\" != input.user.groups[_]"}, - "related_resources": [{ - "description": "documentation", - "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", - }], - "title": "not-equals-in-loop", - }} -} - -test_fail_rule_name_shadows_builtin if { - r := report(`or := 1`) - r == {{ - "category": "bugs", - "description": "Rule name shadows built-in", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/rule-shadows-builtin", "bugs"), - }], - "title": "rule-shadows-builtin", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "or := 1"}, - "level": "error", - }} -} - -test_success_rule_name_does_not_shadows_builtin if { - report(`foo := 1`) == set() -} - -test_fail_rule_named_if if { - r := bugs.report with input as regal.parse_module("policy.rego", `package policy - allow := true if { - input.foo - } - `) - r == {{ - "category": "bugs", - "description": "Rule named \"if\"", - "level": "error", - "location": {"col": 16, "file": "policy.rego", "row": 2, "text": "\tallow := true if {"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/rule-named-if", "bugs"), - }], - "title": "rule-named-if", - }} -} - -report(snippet) := report if { - # regal ignore:external-reference - report := bugs.report with input as ast.with_future_keywords(snippet) -} diff --git a/bundle/regal/rules/bugs/common_test.rego b/bundle/regal/rules/bugs/common_test.rego new file mode 100644 index 00000000..a99a54f7 --- /dev/null +++ b/bundle/regal/rules/bugs/common_test.rego @@ -0,0 +1,18 @@ +package regal.rules.bugs.common_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs + +report_with_fk(snippet) := report if { + # regal ignore:external-reference + report := bugs.report with input as ast.with_future_keywords(snippet) with config.for_rule as {"level": "error"} +} + +report(snippet) := report if { + # regal ignore:external-reference + report := bugs.report with input as ast.policy(snippet) with config.for_rule as {"level": "error"} + print(report) +} diff --git a/bundle/regal/rules/bugs/constant_condition.rego b/bundle/regal/rules/bugs/constant_condition.rego new file mode 100644 index 00000000..66153117 --- /dev/null +++ b/bundle/regal/rules/bugs/constant_condition.rego @@ -0,0 +1,74 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# NOTE: The constant condition checks currently don't do nesting! +# Additionally, there are several other conditions that could be considered +# constant, or if not, redundant... so this rule should be expanded in time + +_operators := {"equal", "gt", "gte", "lt", "lte", "neq"} + +# We could probably include arrays and objects too, as a single compound value +# is not very useful, but it's not as clear cut as scalars, as you could have +# something like {"a": foo(input.x) == "bar"} which is not a constant condition, +# however meaningless it may be. Maybe consider for another rule? +_scalars := {"boolean", "null", "number", "string"} + +_rules_with_bodies := [rule | + some rule in input.rules + not probably_no_body(rule) +] + +# i.e. allow {..}, or allow := true, which expands to allow = true { true } +probably_no_body(rule) if { + count(rule.body) == 1 + rule.body[0].terms.type == "boolean" + rule.body[0].terms.value == true +} + +# METADATA +# title: constant-condition +# description: Constant condition +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/constant-condition +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in _rules_with_bodies + some expr in rule.body + + expr.terms.type in _scalars + + violation := result.fail(rego.metadata.rule(), result.location(expr)) +} + +# METADATA +# title: constant-condition +# description: Constant condition +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/constant-condition +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in _rules_with_bodies + some expr in rule.body + + expr.terms[0].value[0].type == "var" + expr.terms[0].value[0].value in _operators + + expr.terms[1].type in _scalars + expr.terms[2].type in _scalars + + violation := result.fail(rego.metadata.rule(), result.location(expr)) +} diff --git a/bundle/regal/rules/bugs/constant_condition_test.rego b/bundle/regal/rules/bugs/constant_condition_test.rego new file mode 100644 index 00000000..bbe6254b --- /dev/null +++ b/bundle/regal/rules/bugs/constant_condition_test.rego @@ -0,0 +1,49 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs.common_test.report + +test_fail_simple_constant_condition if { + r := report(`allow { + 1 + }`) + r == {{ + "category": "bugs", + "description": "Constant condition", + "location": {"col": 2, "file": "policy.rego", "row": 4, "text": "\t1"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/constant-condition", "bugs"), + }], + "title": "constant-condition", + "level": "error", + }} +} + +test_success_static_condition_probably_generated if { + report(`allow { true }`) == set() +} + +test_fail_operator_constant_condition if { + r := report(`allow { + 1 == 1 + }`) + r == {{ + "category": "bugs", + "description": "Constant condition", + "location": {"col": 2, "file": "policy.rego", "row": 4, "text": "\t1 == 1"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/constant-condition", "bugs"), + }], + "title": "constant-condition", + "level": "error", + }} +} + +test_success_non_constant_condition if { + report(`allow { 1 == input.one }`) == set() +} diff --git a/bundle/regal/rules/bugs/not_equals_in_loop.rego b/bundle/regal/rules/bugs/not_equals_in_loop.rego new file mode 100644 index 00000000..3225ba2c --- /dev/null +++ b/bundle/regal/rules/bugs/not_equals_in_loop.rego @@ -0,0 +1,36 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: not-equals-in-loop +# description: Use of != in loop +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/not-equals-in-loop +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + some expr in rule.body + + expr.terms[0].type == "ref" + expr.terms[0].value[0].type == "var" + expr.terms[0].value[0].value == "neq" + + some neq_term in array.slice(expr.terms, 1, count(expr.terms)) + neq_term.type == "ref" + + some i + neq_term.value[i].type == "var" + startswith(neq_term.value[i].value, "$") + + violation := result.fail(rego.metadata.rule(), result.location(expr.terms[0])) +} diff --git a/bundle/regal/rules/bugs/not_equals_in_loop_test.rego b/bundle/regal/rules/bugs/not_equals_in_loop_test.rego new file mode 100644 index 00000000..08039b42 --- /dev/null +++ b/bundle/regal/rules/bugs/not_equals_in_loop_test.rego @@ -0,0 +1,53 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.rules.bugs.common_test.report +import data.regal.rules.bugs.common_test.report_with_fk + +test_fail_neq_in_loop if { + r := report(`deny { + "admin" != input.user.groups[_] + input.user.groups[_] != "admin" + }`) + r == { + { + "category": "bugs", + "description": "Use of != in loop", + "level": "error", + "location": {"col": 11, "file": "policy.rego", "row": 4, "text": "\t\t\"admin\" != input.user.groups[_]"}, + "related_resources": [{ + "description": "documentation", + "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", + }], + "title": "not-equals-in-loop", + }, + { + "category": "bugs", + "description": "Use of != in loop", + "level": "error", + "location": {"col": 24, "file": "policy.rego", "row": 5, "text": "\t\tinput.user.groups[_] != \"admin\""}, + "related_resources": [{ + "description": "documentation", + "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", + }], + "title": "not-equals-in-loop", + }, + } +} + +test_fail_neq_in_loop_one_liner if { + r := report_with_fk(`deny if "admin" != input.user.groups[_]`) + r == {{ + "category": "bugs", + "description": "Use of != in loop", + "level": "error", + "location": {"col": 17, "file": "policy.rego", "row": 8, "text": "deny if \"admin\" != input.user.groups[_]"}, + "related_resources": [{ + "description": "documentation", + "ref": "https://github.com/StyraInc/regal/blob/main/docs/rules/bugs/not-equals-in-loop.md", + }], + "title": "not-equals-in-loop", + }} +} diff --git a/bundle/regal/rules/bugs/rule_named_if.rego b/bundle/regal/rules/bugs/rule_named_if.rego new file mode 100644 index 00000000..587d06e5 --- /dev/null +++ b/bundle/regal/rules/bugs/rule_named_if.rego @@ -0,0 +1,25 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: rule-named-if +# description: Rule named "if" +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/rule-named-if +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule.head.name == "if" + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} diff --git a/bundle/regal/rules/bugs/rule_named_if_test.rego b/bundle/regal/rules/bugs/rule_named_if_test.rego new file mode 100644 index 00000000..6adcba30 --- /dev/null +++ b/bundle/regal/rules/bugs/rule_named_if_test.rego @@ -0,0 +1,25 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs.common_test.report + +test_fail_rule_named_if if { + r := report(` + allow := true if { + input.foo + }`) + r == {{ + "category": "bugs", + "description": "Rule named \"if\"", + "level": "error", + "location": {"col": 16, "file": "policy.rego", "row": 4, "text": "\tallow := true if {"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/rule-named-if", "bugs"), + }], + "title": "rule-named-if", + }} +} diff --git a/bundle/regal/rules/bugs/rule_shadows_builtin.rego b/bundle/regal/rules/bugs/rule_shadows_builtin.rego new file mode 100644 index 00000000..a231dbc8 --- /dev/null +++ b/bundle/regal/rules/bugs/rule_shadows_builtin.rego @@ -0,0 +1,26 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: rule-shadows-builtin +# description: Rule name shadows built-in +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/rule-shadows-builtin +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule.head.name in ast.builtin_names + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} diff --git a/bundle/regal/rules/bugs/rule_shadows_builtin_test.rego b/bundle/regal/rules/bugs/rule_shadows_builtin_test.rego new file mode 100644 index 00000000..7e254d7f --- /dev/null +++ b/bundle/regal/rules/bugs/rule_shadows_builtin_test.rego @@ -0,0 +1,27 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs +import data.regal.rules.bugs.common_test.report + +test_fail_rule_name_shadows_builtin if { + r := report(`or := 1`) + r == {{ + "category": "bugs", + "description": "Rule name shadows built-in", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/rule-shadows-builtin", "bugs"), + }], + "title": "rule-shadows-builtin", + "location": {"col": 1, "file": "policy.rego", "row": 3, "text": "or := 1"}, + "level": "error", + }} +} + +test_success_rule_name_does_not_shadows_builtin if { + report(`foo := 1`) == set() +} diff --git a/bundle/regal/rules/bugs/top_level_iteration.rego b/bundle/regal/rules/bugs/top_level_iteration.rego new file mode 100644 index 00000000..0d68358a --- /dev/null +++ b/bundle/regal/rules/bugs/top_level_iteration.rego @@ -0,0 +1,36 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: top-level-iteration +# description: Iteration in top-level assignment +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/top-level-iteration +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + + rule.head.value.type == "ref" + + last := regal.last(rule.head.value.value) + last.type == "var" + + illegal_value_ref(last.value) + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} + +_rule_names := {name | name := input.rules[_].head.name} + +# regal ignore:external-reference +illegal_value_ref(value) if not value in _rule_names diff --git a/bundle/regal/rules/bugs/top_level_iteration_test.rego b/bundle/regal/rules/bugs/top_level_iteration_test.rego new file mode 100644 index 00000000..185b89c6 --- /dev/null +++ b/bundle/regal/rules/bugs/top_level_iteration_test.rego @@ -0,0 +1,48 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs +import data.regal.rules.bugs.common_test.report_with_fk + +test_fail_top_level_iteration_wildcard if { + r := report_with_fk(`x := input.foo.bar[_]`) + r == {{ + "category": "bugs", + "description": "Iteration in top-level assignment", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "x := input.foo.bar[_]"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/top-level-iteration", "bugs"), + }], + "title": "top-level-iteration", + "level": "error", + }} +} + +test_fail_top_level_iteration_named_var if { + r := report_with_fk(`x := input.foo.bar[i]`) + r == {{ + "category": "bugs", + "description": "Iteration in top-level assignment", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "x := input.foo.bar[i]"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/top-level-iteration", "bugs"), + }], + "title": "top-level-iteration", + "level": "error", + }} +} + +test_success_top_level_known_var_ref if { + report_with_fk(` + i := "foo" + x := input.foo.bar[i]`) == set() +} + +test_success_top_level_input_ref if { + report_with_fk(`x := input.foo.bar[input.y]`) == set() +} diff --git a/bundle/regal/rules/bugs/unused_return_value.rego b/bundle/regal/rules/bugs/unused_return_value.rego new file mode 100644 index 00000000..f6cebce4 --- /dev/null +++ b/bundle/regal/rules/bugs/unused_return_value.rego @@ -0,0 +1,35 @@ +package regal.rules.bugs + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.opa +import data.regal.result + +# METADATA +# title: unused-return-value +# description: Non-boolean return value unused +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/unused-return-value +# custom: +# category: bugs +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + some expr in rule.body + + expr.terms[0].type == "ref" + expr.terms[0].value[0].type == "var" + + ref_name := expr.terms[0].value[0].value + ref_name in ast.builtin_names + + opa.builtins[ref_name].result.type != "boolean" + + violation := result.fail(rego.metadata.rule(), result.location(expr.terms[0])) +} diff --git a/bundle/regal/rules/bugs/unused_return_value_test.rego b/bundle/regal/rules/bugs/unused_return_value_test.rego new file mode 100644 index 00000000..4833dcfb --- /dev/null +++ b/bundle/regal/rules/bugs/unused_return_value_test.rego @@ -0,0 +1,33 @@ +package regal.rules.bugs_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.bugs +import data.regal.rules.bugs.common_test.report_with_fk + +test_fail_unused_return_value if { + r := report_with_fk(`allow { + indexof("s", "s") + }`) + r == {{ + "category": "bugs", + "description": "Non-boolean return value unused", + "level": "error", + "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tindexof(\"s\", \"s\")"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/unused-return-value", "bugs"), + }], + "title": "unused-return-value", + }} +} + +test_success_unused_boolean_return_value if { + report_with_fk(`allow { startswith("s", "s") }`) == set() +} + +test_success_return_value_assigned if { + report_with_fk(`allow { x := indexof("s", "s") }`) == set() +} diff --git a/bundle/regal/rules/imports/avoid_importing_input.rego b/bundle/regal/rules/imports/avoid_importing_input.rego new file mode 100644 index 00000000..baf6f431 --- /dev/null +++ b/bundle/regal/rules/imports/avoid_importing_input.rego @@ -0,0 +1,34 @@ +package regal.rules.imports + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: avoid-importing-input +# description: Avoid importing input +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/avoid-importing-input +# custom: +# category: imports +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some imported in input.imports + + imported.path.value[0].value == "input" + + # Allow aliasing input, eg `import input as tfplan`: + not _aliased_input(imported) + + violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) +} + +_aliased_input(imported) if { + count(imported.path.value) == 1 + imported.alias +} diff --git a/bundle/regal/rules/imports/avoid_importing_input_test.rego b/bundle/regal/rules/imports/avoid_importing_input_test.rego new file mode 100644 index 00000000..4056c3c2 --- /dev/null +++ b/bundle/regal/rules/imports/avoid_importing_input_test.rego @@ -0,0 +1,39 @@ +package regal.rules.imports_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports.common_test.report + +test_fail_import_input if { + report(`import input.foo`) == {{ + "category": "imports", + "description": "Avoid importing input", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/avoid-importing-input", "imports"), + }], + "title": "avoid-importing-input", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import input.foo`}, + "level": "error", + }} +} + +test_sucess_import_aliased_input if { + report(`import input as tfplan`) == set() +} + +test_fail_import_input_aliased_attribute if { + report(`import input.foo.bar as barbar`) == {{ + "category": "imports", + "description": "Avoid importing input", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/avoid-importing-input", "imports"), + }], + "title": "avoid-importing-input", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import input.foo.bar as barbar`}, + "level": "error", + }} +} diff --git a/bundle/regal/rules/imports/common_test.rego b/bundle/regal/rules/imports/common_test.rego new file mode 100644 index 00000000..7fed5751 --- /dev/null +++ b/bundle/regal/rules/imports/common_test.rego @@ -0,0 +1,17 @@ +package regal.rules.imports.common_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports + +report_with_fk(snippet) := report if { + # regal ignore:external-reference + report := imports.report with input as ast.with_future_keywords(snippet) with config.for_rule as {"level": "error"} +} + +report(snippet) := report if { + # regal ignore:external-reference + report := imports.report with input as ast.policy(snippet) with config.for_rule as {"level": "error"} +} diff --git a/bundle/regal/rules/imports/implicit_future_keywords.rego b/bundle/regal/rules/imports/implicit_future_keywords.rego new file mode 100644 index 00000000..3648eee1 --- /dev/null +++ b/bundle/regal/rules/imports/implicit_future_keywords.rego @@ -0,0 +1,33 @@ +package regal.rules.imports + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: implicit-future-keywords +# description: Use explicit future keyword imports +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/implicit-future-keywords +# custom: +# category: imports +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some imported in input.imports + + imported.path.type == "ref" + + count(imported.path.value) == 2 + + imported.path.value[0].type == "var" + imported.path.value[0].value == "future" + imported.path.value[1].type == "string" + imported.path.value[1].value == "keywords" + + violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) +} diff --git a/bundle/regal/rules/imports/implicit_future_keywords_test.rego b/bundle/regal/rules/imports/implicit_future_keywords_test.rego new file mode 100644 index 00000000..3e358b58 --- /dev/null +++ b/bundle/regal/rules/imports/implicit_future_keywords_test.rego @@ -0,0 +1,34 @@ +package regal.rules.imports_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports +import data.regal.rules.imports.common_test.report + +test_fail_future_keywords_import_wildcard if { + report(`import future.keywords`) == {{ + "category": "imports", + "description": "Use explicit future keyword imports", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/implicit-future-keywords", "imports"), + }], + "title": "implicit-future-keywords", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import future.keywords`}, + "level": "error", + }} +} + +test_success_future_keywords_import_specific if { + report(`import future.keywords.contains`) == set() +} + +test_success_future_keywords_import_specific_many if { + report(` + import future.keywords.contains + import future.keywords.if + import future.keywords.in + `) == set() +} diff --git a/bundle/regal/rules/imports/import_shadows_import.rego b/bundle/regal/rules/imports/import_shadows_import.rego new file mode 100644 index 00000000..fc0d856a --- /dev/null +++ b/bundle/regal/rules/imports/import_shadows_import.rego @@ -0,0 +1,39 @@ +package regal.rules.imports + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# regular import +_ident(imported) := regal.last(path).value if { + not imported.alias + path := imported.path.value +} + +# aliased import +_ident(imported) := imported.alias + +_identifiers := [_ident(imported) | + some imported in input.imports +] + +# METADATA +# title: import-shadows-import +# description: Import shadows another import +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/import-shadows-import +# custom: +# category: imports +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some i, identifier in _identifiers + + identifier in array.slice(_identifiers, 0, i) + + violation := result.fail(rego.metadata.rule(), result.location(input.imports[i].path.value[0])) +} diff --git a/bundle/regal/rules/imports/import_shadows_import_test.rego b/bundle/regal/rules/imports/import_shadows_import_test.rego new file mode 100644 index 00000000..f5577135 --- /dev/null +++ b/bundle/regal/rules/imports/import_shadows_import_test.rego @@ -0,0 +1,43 @@ +package regal.rules.imports_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports +import data.regal.rules.imports.common_test.report + +test_fail_duplicate_import if { + r := report(` +import data.foo +import data.foo + `) + r == {{ + "category": "imports", + "description": "Import shadows another import", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/import-shadows-import", "imports"), + }], + "title": "import-shadows-import", + "location": {"col": 8, "file": "policy.rego", "row": 5, "text": `import data.foo`}, + "level": "error", + }} +} + +test_fail_duplicate_import_alias if { + report(` +import data.foo +import data.bar as foo + `) == {{ + "category": "imports", + "description": "Import shadows another import", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/import-shadows-import", "imports"), + }], + "title": "import-shadows-import", + "location": {"col": 8, "file": "policy.rego", "row": 5, "text": `import data.bar as foo`}, + "level": "error", + }} +} diff --git a/bundle/regal/rules/imports/imports.rego b/bundle/regal/rules/imports/imports.rego deleted file mode 100644 index f33fe3d4..00000000 --- a/bundle/regal/rules/imports/imports.rego +++ /dev/null @@ -1,128 +0,0 @@ -package regal.rules.imports - -import future.keywords.contains -import future.keywords.if -import future.keywords.in - -import data.regal.config -import data.regal.result - -_identifiers := [_ident(imported) | - some imported in input.imports -] - -# regular import -_ident(imported) := regal.last(path).value if { - not imported.alias - path := imported.path.value -} - -# aliased import -_ident(imported) := imported.alias - -# METADATA -# title: implicit-future-keywords -# description: Use explicit future keyword imports -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/implicit-future-keywords -# custom: -# category: imports -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some imported in input.imports - - imported.path.type == "ref" - - count(imported.path.value) == 2 - - imported.path.value[0].type == "var" - imported.path.value[0].value == "future" - imported.path.value[1].type == "string" - imported.path.value[1].value == "keywords" - - violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) -} - -# METADATA -# title: avoid-importing-input -# description: Avoid importing input -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/avoid-importing-input -# custom: -# category: imports -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some imported in input.imports - - imported.path.value[0].value == "input" - - # Allow aliasing input, eg `import input as tfplan`: - not _aliased_input(imported) - - violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) -} - -_aliased_input(imported) if { - count(imported.path.value) == 1 - imported.alias -} - -# METADATA -# title: redundant-data-import -# description: Redundant import of data -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/redundant-data-import -# custom: -# category: imports -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some imported in input.imports - - count(imported.path.value) == 1 - - imported.path.value[0].value == "data" - - violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) -} - -# METADATA -# title: import-shadows-import -# description: Import shadows another import -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/import-shadows-import -# custom: -# category: imports -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some i, identifier in _identifiers - - identifier in array.slice(_identifiers, 0, i) - - violation := result.fail(rego.metadata.rule(), result.location(input.imports[i].path.value[0])) -} - -# METADATA -# title: redundant-alias -# description: Redundant alias -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/redundant-alias -# custom: -# category: imports -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some imported in input.imports - - regal.last(imported.path.value).value == imported.alias - - violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) -} diff --git a/bundle/regal/rules/imports/imports_test.rego b/bundle/regal/rules/imports/imports_test.rego deleted file mode 100644 index 95d19984..00000000 --- a/bundle/regal/rules/imports/imports_test.rego +++ /dev/null @@ -1,156 +0,0 @@ -package regal.rules.imports_test - -import future.keywords.if - -import data.regal.ast -import data.regal.config -import data.regal.rules.imports - -test_fail_future_keywords_import_wildcard if { - report(`import future.keywords`) == {{ - "category": "imports", - "description": "Use explicit future keyword imports", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/implicit-future-keywords", "imports"), - }], - "title": "implicit-future-keywords", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import future.keywords`}, - "level": "error", - }} -} - -test_success_future_keywords_import_specific if { - report(`import future.keywords.contains`) == set() -} - -test_success_future_keywords_import_specific_many if { - report(` - import future.keywords.contains - import future.keywords.if - import future.keywords.in - `) == set() -} - -test_fail_import_input if { - report(`import input.foo`) == {{ - "category": "imports", - "description": "Avoid importing input", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/avoid-importing-input", "imports"), - }], - "title": "avoid-importing-input", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import input.foo`}, - "level": "error", - }} -} - -test_sucess_import_aliased_input if { - report(`import input as tfplan`) == set() -} - -test_fail_import_input_aliased_attribute if { - report(`import input.foo.bar as barbar`) == {{ - "category": "imports", - "description": "Avoid importing input", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/avoid-importing-input", "imports"), - }], - "title": "avoid-importing-input", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import input.foo.bar as barbar`}, - "level": "error", - }} -} - -test_fail_import_data if { - report(`import data`) == {{ - "category": "imports", - "description": "Redundant import of data", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/redundant-data-import", "imports"), - }], - "title": "redundant-data-import", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import data`}, - "level": "error", - }} -} - -test_fail_import_data_aliased if { - report(`import data as d`) == {{ - "category": "imports", - "description": "Redundant import of data", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/redundant-data-import", "imports"), - }], - "title": "redundant-data-import", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import data as d`}, - "level": "error", - }} -} - -test_success_import_data_path if { - report(`import data.something`) == set() -} - -test_fail_duplicate_import if { - r := report(` -import data.foo -import data.foo - `) - r == {{ - "category": "imports", - "description": "Import shadows another import", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/import-shadows-import", "imports"), - }], - "title": "import-shadows-import", - "location": {"col": 8, "file": "policy.rego", "row": 5, "text": `import data.foo`}, - "level": "error", - }} -} - -test_fail_duplicate_import_alias if { - report(` -import data.foo -import data.bar as foo - `) == {{ - "category": "imports", - "description": "Import shadows another import", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/import-shadows-import", "imports"), - }], - "title": "import-shadows-import", - "location": {"col": 8, "file": "policy.rego", "row": 5, "text": `import data.bar as foo`}, - "level": "error", - }} -} - -test_fail_redundant_alias if { - r := report(`import data.foo.bar as bar`) - r == {{ - "category": "imports", - "description": "Redundant alias", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/redundant-alias", "imports"), - }], - "title": "redundant-alias", - "location": {"col": 8, "file": "policy.rego", "row": 3, "text": "import data.foo.bar as bar"}, - "level": "error", - }} -} - -test_success_not_redundant_alias if { - report(`import data.foo.bar as valid`) == set() -} - -report(snippet) := report if { - # regal ignore:external-reference - report := imports.report with input as ast.policy(snippet) -} diff --git a/bundle/regal/rules/imports/redundant_alias.rego b/bundle/regal/rules/imports/redundant_alias.rego new file mode 100644 index 00000000..ac913856 --- /dev/null +++ b/bundle/regal/rules/imports/redundant_alias.rego @@ -0,0 +1,26 @@ +package regal.rules.imports + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: redundant-alias +# description: Redundant alias +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/redundant-alias +# custom: +# category: imports +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some imported in input.imports + + regal.last(imported.path.value).value == imported.alias + + violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) +} diff --git a/bundle/regal/rules/imports/redundant_alias_test.rego b/bundle/regal/rules/imports/redundant_alias_test.rego new file mode 100644 index 00000000..6efb545e --- /dev/null +++ b/bundle/regal/rules/imports/redundant_alias_test.rego @@ -0,0 +1,27 @@ +package regal.rules.imports_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports +import data.regal.rules.imports.common_test.report + +test_fail_redundant_alias if { + r := report(`import data.foo.bar as bar`) + r == {{ + "category": "imports", + "description": "Redundant alias", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/redundant-alias", "imports"), + }], + "title": "redundant-alias", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": "import data.foo.bar as bar"}, + "level": "error", + }} +} + +test_success_not_redundant_alias if { + report(`import data.foo.bar as valid`) == set() +} diff --git a/bundle/regal/rules/imports/redundant_data_import.rego b/bundle/regal/rules/imports/redundant_data_import.rego new file mode 100644 index 00000000..0247138d --- /dev/null +++ b/bundle/regal/rules/imports/redundant_data_import.rego @@ -0,0 +1,28 @@ +package regal.rules.imports + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: redundant-data-import +# description: Redundant import of data +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/redundant-data-import +# custom: +# category: imports +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some imported in input.imports + + count(imported.path.value) == 1 + + imported.path.value[0].value == "data" + + violation := result.fail(rego.metadata.rule(), result.location(imported.path.value[0])) +} diff --git a/bundle/regal/rules/imports/redundant_data_import_test.rego b/bundle/regal/rules/imports/redundant_data_import_test.rego new file mode 100644 index 00000000..7729364a --- /dev/null +++ b/bundle/regal/rules/imports/redundant_data_import_test.rego @@ -0,0 +1,40 @@ +package regal.rules.imports_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.imports +import data.regal.rules.imports.common_test.report + +test_fail_import_data if { + report(`import data`) == {{ + "category": "imports", + "description": "Redundant import of data", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/redundant-data-import", "imports"), + }], + "title": "redundant-data-import", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import data`}, + "level": "error", + }} +} + +test_fail_import_data_aliased if { + report(`import data as d`) == {{ + "category": "imports", + "description": "Redundant import of data", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/redundant-data-import", "imports"), + }], + "title": "redundant-data-import", + "location": {"col": 8, "file": "policy.rego", "row": 3, "text": `import data as d`}, + "level": "error", + }} +} + +test_success_import_data_path if { + report(`import data.something`) == set() +} diff --git a/bundle/regal/rules/style/avoid_get_and_list_prefix.rego b/bundle/regal/rules/style/avoid_get_and_list_prefix.rego new file mode 100644 index 00000000..a444a566 --- /dev/null +++ b/bundle/regal/rules/style/avoid_get_and_list_prefix.rego @@ -0,0 +1,25 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: avoid-get-and-list-prefix +# description: Avoid get_ and list_ prefix for rules and functions +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/avoid-get-and-list-prefix +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + strings.any_prefix_match(rule.head.name, {"get_", "list_"}) + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} diff --git a/bundle/regal/rules/style/avoid_get_and_list_prefix_test.rego b/bundle/regal/rules/style/avoid_get_and_list_prefix_test.rego new file mode 100644 index 00000000..7275713b --- /dev/null +++ b/bundle/regal/rules/style/avoid_get_and_list_prefix_test.rego @@ -0,0 +1,41 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_rule_name_starts_with_get if { + r := report(`get_foo := 1`) + r == {{ + "category": "style", + "description": "Avoid get_ and list_ prefix for rules and functions", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/avoid-get-and-list-prefix", "style"), + }], + "title": "avoid-get-and-list-prefix", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "get_foo := 1"}, + "level": "error", + }} +} + +test_fail_function_name_starts_with_list if { + r := report(`list_users(datasource) := ["we", "have", "no", "users"]`) + r == {{ + "category": "style", + "description": "Avoid get_ and list_ prefix for rules and functions", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/avoid-get-and-list-prefix", "style"), + }], + "title": "avoid-get-and-list-prefix", + "location": { + "col": 1, "file": "policy.rego", "row": 8, + "text": `list_users(datasource) := ["we", "have", "no", "users"]`, + }, + "level": "error", + }} +} diff --git a/bundle/regal/rules/style/common_test.rego b/bundle/regal/rules/style/common_test.rego new file mode 100644 index 00000000..5412a288 --- /dev/null +++ b/bundle/regal/rules/style/common_test.rego @@ -0,0 +1,13 @@ +package regal.rules.style.common_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style + +report(snippet) := report if { + # regal ignore:external-reference + report := style.report with input as ast.with_future_keywords(snippet) + with config.for_rule as {"level": "error", "max-line-length": 80} +} diff --git a/bundle/regal/rules/style/external_reference.rego b/bundle/regal/rules/style/external_reference.rego new file mode 100644 index 00000000..6877d799 --- /dev/null +++ b/bundle/regal/rules/style/external_reference.rego @@ -0,0 +1,70 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: external-reference +# description: Reference to input, data or rule ref in function body +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/external-reference +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule.head.args + + named_args := {arg.value | some arg in rule.head.args; arg.type == "var"} + own_vars := {v.value | some v in ast.find_vars(rule.body)} + + allowed_refs := named_args | own_vars + + some expr in rule.body + + is_array(expr.terms) + + some term in expr.terms + + term.type == "var" + not term.value in allowed_refs + + violation := result.fail(rego.metadata.rule(), result.location(term)) +} + +# METADATA +# title: external-reference +# description: Reference to input, data or rule ref in function body +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/external-reference +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule.head.args + + named_args := {arg.value | some arg in rule.head.args; arg.type == "var"} + own_vars := {v.value | some v in ast.find_vars(rule.body)} + + allowed_refs := named_args | own_vars + + some expr in rule.body + + is_object(expr.terms) + + terms := expr.terms.value + terms[0].type == "var" + not terms[0].value in allowed_refs + + violation := result.fail(rego.metadata.rule(), result.location(terms[0])) +} diff --git a/bundle/regal/rules/style/external_reference_test.rego b/bundle/regal/rules/style/external_reference_test.rego new file mode 100644 index 00000000..e0cb4df9 --- /dev/null +++ b/bundle/regal/rules/style/external_reference_test.rego @@ -0,0 +1,74 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_function_references_input if { + report(`f(_) { input.foo }`) == {{ + "category": "style", + "description": "Reference to input, data or rule ref in function body", + "location": {"col": 8, "file": "policy.rego", "row": 8, "text": `f(_) { input.foo }`}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), + }], + "title": "external-reference", + "level": "error", + }} +} + +test_fail_function_references_data if { + report(`f(_) { data.foo }`) == {{ + "category": "style", + "description": "Reference to input, data or rule ref in function body", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), + }], + "title": "external-reference", + "location": {"col": 8, "file": "policy.rego", "row": 8, "text": `f(_) { data.foo }`}, + "level": "error", + }} +} + +test_fail_function_references_rule if { + r := report(` +foo := "bar" + +f(x, y) { + x == 5 + y == foo +} + `) + r == {{ + "category": "style", + "description": "Reference to input, data or rule ref in function body", + "location": {"col": 7, "file": "policy.rego", "row": 13, "text": ` y == foo`}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), + }], + "title": "external-reference", + "level": "error", + }} +} + +test_success_function_references_no_input_or_data if { + report(`f(x) { x == true }`) == set() +} + +test_success_function_references_no_input_or_data_reverse if { + report(`f(x) { true == x }`) == set() +} + +test_success_function_references_only_own_vars if { + report(`f(x) { y := x; y == 10 }`) == set() +} + +test_success_function_references_only_own_vars_nested if { + report(`f(x, z) { y := x; y == [1, 2, z]}`) == set() +} diff --git a/bundle/regal/rules/style/function_arg_return.rego b/bundle/regal/rules/style/function_arg_return.rego new file mode 100644 index 00000000..18bd20b4 --- /dev/null +++ b/bundle/regal/rules/style/function_arg_return.rego @@ -0,0 +1,45 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: function-arg-return +# description: Function argument used for return value +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/function-arg-return +# custom: +# category: style +report contains violation if { + cfg := config.for_rule(rego.metadata.rule()) + cfg.level != "ignore" + + except_functions := array.concat( + object.get(cfg, "except-functions", []), + ["print"], + ) + + # rule ignoring itself :) + # regal ignore:function-arg-return + walk(input.rules, [path, value]) + + regal.last(path) == "terms" + + value[0].type == "ref" + value[0].value[0].type == "var" + + fn_name := value[0].value[0].value + + not fn_name in except_functions + fn_name in ast.all_function_names + + ast.function_ret_in_args(fn_name, value) + + violation := result.fail(rego.metadata.rule(), result.location(regal.last(value))) +} diff --git a/bundle/regal/rules/style/function_arg_return_test.rego b/bundle/regal/rules/style/function_arg_return_test.rego new file mode 100644 index 00000000..e7ec6555 --- /dev/null +++ b/bundle/regal/rules/style/function_arg_return_test.rego @@ -0,0 +1,32 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_function_arg_return_value if { + r := report(`foo := i { indexof("foo", "o", i) }`) + r == {{ + "category": "style", + "description": "Function argument used for return value", + "level": "error", + "location": {"col": 32, "file": "policy.rego", "row": 8, "text": "foo := i { indexof(\"foo\", \"o\", i) }"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/function-arg-return", "style"), + }], + "title": "function-arg-return", + }} +} + +test_success_function_arg_return_value_except_function if { + r := style.report with input as ast.with_future_keywords(`foo := i { indexof("foo", "o", i) }`) + with config.for_rule as { + "level": "error", + "except-functions": ["indexof"], + } + r == set() +} diff --git a/bundle/regal/rules/style/line_length.rego b/bundle/regal/rules/style/line_length.rego new file mode 100644 index 00000000..623695eb --- /dev/null +++ b/bundle/regal/rules/style/line_length.rego @@ -0,0 +1,38 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: line-length +# description: Line too long +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/line-length +# custom: +# category: style +report contains violation if { + cfg := config.for_rule(rego.metadata.rule()) + + cfg.level != "ignore" + + some i, line in input.regal.file.lines + + line_length := count(line) + line_length > cfg["max-line-length"] + + violation := result.fail( + rego.metadata.rule(), + {"location": { + "file": input.regal.file.name, + "row": i + 1, + "col": line_length, + "text": input.regal.file.lines[i], + }}, + ) +} diff --git a/bundle/regal/rules/style/line_length_test.rego b/bundle/regal/rules/style/line_length_test.rego new file mode 100644 index 00000000..4c6e8959 --- /dev/null +++ b/bundle/regal/rules/style/line_length_test.rego @@ -0,0 +1,32 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_line_too_long if { + r := report(`allow { +foo == bar; bar == baz; [a, b, c, d, e, f] := [1, 2, 3, 4, 5, 6]; qux := [q | some q in input.nonsense] + }`) + r == {{ + "category": "style", + "description": "Line too long", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/line-length", "style"), + }], + "title": "line-length", + "location": { + "col": 103, "file": "policy.rego", "row": 9, + "text": `foo == bar; bar == baz; [a, b, c, d, e, f] := [1, 2, 3, 4, 5, 6]; qux := [q | some q in input.nonsense]`, + }, + "level": "error", + }} +} + +test_success_line_not_too_long if { + report(`allow { "foo" == "bar" }`) == set() +} diff --git a/bundle/regal/rules/style/prefer_snake_case.rego b/bundle/regal/rules/style/prefer_snake_case.rego new file mode 100644 index 00000000..0b670f12 --- /dev/null +++ b/bundle/regal/rules/style/prefer_snake_case.rego @@ -0,0 +1,44 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result +import data.regal.util + +# METADATA +# title: prefer-snake-case +# description: Prefer snake_case for names +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/prefer-snake-case +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + not util.is_snake_case(rule.head.name) + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} + +# METADATA +# title: prefer-snake-case +# description: Prefer snake_case for names +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/prefer-snake-case +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some var in ast.find_vars(input.rules) + not util.is_snake_case(var.value) + + violation := result.fail(rego.metadata.rule(), result.location(var)) +} diff --git a/bundle/regal/rules/style/prefer_snake_case_test.rego b/bundle/regal/rules/style/prefer_snake_case_test.rego new file mode 100644 index 00000000..145eb9ad --- /dev/null +++ b/bundle/regal/rules/style/prefer_snake_case_test.rego @@ -0,0 +1,123 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +snake_case_violation := { + "category": "style", + "description": "Prefer snake_case for names", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/prefer-snake-case", "style"), + }], + "title": "prefer-snake-case", + "level": "error", +} + +test_fail_camel_cased_rule_name if { + report(`camelCase := 5`) == {object.union( + snake_case_violation, + {"location": {"col": 1, "file": "policy.rego", "row": 8, "text": `camelCase := 5`}}, + )} +} + +test_success_snake_cased_rule_name if { + report(`snake_case := 5`) == set() +} + +test_fail_camel_cased_some_declaration if { + report(`p {some fooBar; input[fooBar]}`) == {object.union( + snake_case_violation, + {"location": {"col": 9, "file": "policy.rego", "row": 8, "text": `p {some fooBar; input[fooBar]}`}}, + )} +} + +test_success_snake_cased_some_declaration if { + report(`p {some foo_bar; input[foo_bar]}`) == set() +} + +test_fail_camel_cased_multiple_some_declaration if { + report(`p {some x, foo_bar, fooBar; x = 1; foo_bar = 2; input[fooBar]}`) == {object.union( + snake_case_violation, + {"location": { + "col": 21, "file": "policy.rego", "row": 8, + "text": `p {some x, foo_bar, fooBar; x = 1; foo_bar = 2; input[fooBar]}`, + }}, + )} +} + +test_success_snake_cased_multiple_some_declaration if { + report(`p {some x, foo_bar; x = 5; input[foo_bar]}`) == set() +} + +test_fail_camel_cased_var_assignment if { + report(`allow { camelCase := 5 }`) == {object.union( + snake_case_violation, + {"location": {"col": 9, "file": "policy.rego", "row": 8, "text": `allow { camelCase := 5 }`}}, + )} +} + +test_fail_camel_cased_multiple_var_assignment if { + report(`allow { snake_case := "foo"; camelCase := 5 }`) == {object.union( + snake_case_violation, + {"location": { + "col": 30, "file": "policy.rego", "row": 8, + "text": `allow { snake_case := "foo"; camelCase := 5 }`, + }}, + )} +} + +test_success_snake_cased_var_assignment if { + report(`allow { snake_case := 5 }`) == set() +} + +test_fail_camel_cased_some_in_value if { + report(`allow { some cC in input }`) == {object.union( + snake_case_violation, + {"location": {"col": 14, "file": "policy.rego", "row": 8, "text": `allow { some cC in input }`}}, + )} +} + +test_fail_camel_cased_some_in_key_value if { + report(`allow { some cC, sc in input }`) == {object.union( + snake_case_violation, + {"location": {"col": 14, "file": "policy.rego", "row": 8, "text": `allow { some cC, sc in input }`}}, + )} +} + +test_fail_camel_cased_some_in_key_value_2 if { + report(`allow { some sc, cC in input }`) == {object.union( + snake_case_violation, + {"location": {"col": 18, "file": "policy.rego", "row": 8, "text": `allow { some sc, cC in input }`}}, + )} +} + +test_success_snake_cased_some_in if { + report(`allow { some sc in input }`) == set() +} + +test_fail_camel_cased_every_value if { + report(`allow { every cC in input { cC == 1 } }`) == {object.union( + snake_case_violation, + {"location": {"col": 15, "file": "policy.rego", "row": 8, "text": `allow { every cC in input { cC == 1 } }`}}, + )} +} + +test_fail_camel_cased_every_key if { + r := report(`allow { every cC, sc in input { cC == 1; sc == 2 } }`) + r == {object.union( + snake_case_violation, + {"location": { + "col": 15, "file": "policy.rego", "row": 8, + "text": `allow { every cC, sc in input { cC == 1; sc == 2 } }`, + }}, + )} +} + +test_success_snake_cased_every if { + report(`allow { every sc in input { sc == 1 } }`) == set() +} diff --git a/bundle/regal/rules/style/style.rego b/bundle/regal/rules/style/style.rego deleted file mode 100644 index 75d91453..00000000 --- a/bundle/regal/rules/style/style.rego +++ /dev/null @@ -1,349 +0,0 @@ -package regal.rules.style - -import future.keywords.contains -import future.keywords.if -import future.keywords.in - -import data.regal.ast -import data.regal.config -import data.regal.result -import data.regal.util - -# METADATA -# title: prefer-snake-case -# description: Prefer snake_case for names -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/prefer-snake-case -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - not util.is_snake_case(rule.head.name) - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} - -# METADATA -# title: prefer-snake-case -# description: Prefer snake_case for names -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/prefer-snake-case -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some var in ast.find_vars(input.rules) - not util.is_snake_case(var.value) - - violation := result.fail(rego.metadata.rule(), result.location(var)) -} - -# METADATA -# title: use-in-operator -# description: Use in to check for membership -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/use-in-operator -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some expr in eq_exprs - - expr.terms[1].type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} - expr.terms[2].type == "ref" - - last := regal.last(expr.terms[2].value) - - last.type == "var" - startswith(last.value, "$") - - violation := result.fail(rego.metadata.rule(), result.location(expr.terms[2].value[0])) -} - -# METADATA -# title: use-in-operator -# description: Use in to check for membership -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/use-in-operator -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some expr in eq_exprs - - expr.terms[1].type == "ref" - expr.terms[2].type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} - - last := regal.last(expr.terms[1].value) - - last.type == "var" - startswith(last.value, "$") - - violation := result.fail(rego.metadata.rule(), result.location(expr.terms[1].value[0])) -} - -eq_exprs contains expr if { - config.for_rule({"category": "style", "title": "use-in-operator"}).level != "ignore" - - some rule in input.rules - some expr in rule.body - - expr.terms[0].type == "ref" - expr.terms[0].value[0].type == "var" - expr.terms[0].value[0].value == "equal" -} - -# METADATA -# title: line-length -# description: Line too long -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/line-length -# custom: -# category: style -report contains violation if { - cfg := config.for_rule(rego.metadata.rule()) - - cfg.level != "ignore" - - some i, line in input.regal.file.lines - - line_length := count(line) - line_length > cfg["max-line-length"] - - violation := result.fail( - rego.metadata.rule(), - {"location": { - "file": input.regal.file.name, - "row": i + 1, - "col": line_length, - "text": input.regal.file.lines[i], - }}, - ) -} - -# METADATA -# title: use-assignment-operator -# description: Prefer := over = for assignment -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/use-assignment-operator -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule["default"] == true - not rule.head.assign - - violation := result.fail(rego.metadata.rule(), result.location(rule)) -} - -# METADATA -# title: use-assignment-operator -# description: Prefer := over = for assignment -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/use-assignment-operator -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule.head.key - rule.head.value - not rule.head.assign - - violation := result.fail(rego.metadata.rule(), result.location(rule.head.ref[0])) -} - -# For comments, OPA uses capital-cases Text and Location rather -# than text and location. As fixing this would potentially break -# things, we need to take it into consideration here. - -todo_identifiers := ["todo", "TODO", "fixme", "FIXME"] - -todo_pattern := sprintf(`^\s*(%s)`, [concat("|", todo_identifiers)]) - -# METADATA -# title: todo-comment -# description: Avoid TODO comments -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/todo-comment -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some comment in input.comments - text := base64.decode(comment.Text) - regex.match(todo_pattern, text) - - violation := result.fail(rego.metadata.rule(), result.location(comment)) -} - -# METADATA -# title: external-reference -# description: Reference to input, data or rule ref in function body -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/external-reference -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule.head.args - - named_args := {arg.value | some arg in rule.head.args; arg.type == "var"} - own_vars := {v.value | some v in ast.find_vars(rule.body)} - - allowed_refs := named_args | own_vars - - some expr in rule.body - - is_array(expr.terms) - - some term in expr.terms - - term.type == "var" - not term.value in allowed_refs - - violation := result.fail(rego.metadata.rule(), result.location(term)) -} - -# METADATA -# title: external-reference -# description: Reference to input, data or rule ref in function body -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/external-reference -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - rule.head.args - - named_args := {arg.value | some arg in rule.head.args; arg.type == "var"} - own_vars := {v.value | some v in ast.find_vars(rule.body)} - - allowed_refs := named_args | own_vars - - some expr in rule.body - - is_object(expr.terms) - - terms := expr.terms.value - terms[0].type == "var" - not terms[0].value in allowed_refs - - violation := result.fail(rego.metadata.rule(), result.location(terms[0])) -} - -# METADATA -# title: avoid-get-and-list-prefix -# description: Avoid get_ and list_ prefix for rules and functions -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/avoid-get-and-list-prefix -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - strings.any_prefix_match(rule.head.name, {"get_", "list_"}) - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} - -# METADATA -# title: unconditional-assignment -# description: Unconditional assignment in rule body -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/unconditional-assignment -# custom: -# category: style -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - - # Single expression in rule body - # There's going to be a few cases where more expressions - # are in the body and it still "unconditional", like e.g - # a `print` call.. but let's keep it simple for now - count(rule.body) == 1 - - # Var assignment in rule head - rule.head.value.type == "var" - rule_head_var := rule.head.value.value - - # If a `with` statement is found in body, back out, as these - # can't be moved to the rule head - not rule.body[0]["with"] - - # Which is an assignment (= or :=) - terms := rule.body[0].terms - terms[0].type == "ref" - terms[0].value[0].type == "var" - terms[0].value[0].value in {"eq", "assign"} - - # Of var declared in rule head - terms[1].type == "var" - terms[1].value == rule_head_var - - violation := result.fail(rego.metadata.rule(), result.location(terms[1])) -} - -# METADATA -# title: function-arg-return -# description: Function argument used for return value -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/function-arg-return -# custom: -# category: style -report contains violation if { - cfg := config.for_rule(rego.metadata.rule()) - cfg.level != "ignore" - - except_functions := array.concat( - object.get(cfg, "except-functions", []), - ["print"], - ) - - # rule ignoring itself :) - # regal ignore:function-arg-return - walk(input.rules, [path, value]) - - regal.last(path) == "terms" - - value[0].type == "ref" - value[0].value[0].type == "var" - - fn_name := value[0].value[0].value - - not fn_name in except_functions - fn_name in ast.all_function_names - - ast.function_ret_in_args(fn_name, value) - - violation := result.fail(rego.metadata.rule(), result.location(regal.last(value))) -} diff --git a/bundle/regal/rules/style/style_test.rego b/bundle/regal/rules/style/style_test.rego deleted file mode 100644 index 2d9710e5..00000000 --- a/bundle/regal/rules/style/style_test.rego +++ /dev/null @@ -1,572 +0,0 @@ -package regal.rules.style_test - -import future.keywords.if - -import data.regal.ast -import data.regal.config -import data.regal.rules.style - -snake_case_violation := { - "category": "style", - "description": "Prefer snake_case for names", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/prefer-snake-case", "style"), - }], - "title": "prefer-snake-case", - "level": "error", -} - -test_fail_camel_cased_rule_name if { - report(`camelCase := 5`) == {object.union( - snake_case_violation, - {"location": {"col": 1, "file": "policy.rego", "row": 8, "text": `camelCase := 5`}}, - )} -} - -test_success_snake_cased_rule_name if { - report(`snake_case := 5`) == set() -} - -test_fail_camel_cased_some_declaration if { - report(`p {some fooBar; input[fooBar]}`) == {object.union( - snake_case_violation, - {"location": {"col": 9, "file": "policy.rego", "row": 8, "text": `p {some fooBar; input[fooBar]}`}}, - )} -} - -test_success_snake_cased_some_declaration if { - report(`p {some foo_bar; input[foo_bar]}`) == set() -} - -test_fail_camel_cased_multiple_some_declaration if { - report(`p {some x, foo_bar, fooBar; x = 1; foo_bar = 2; input[fooBar]}`) == {object.union( - snake_case_violation, - {"location": { - "col": 21, "file": "policy.rego", "row": 8, - "text": `p {some x, foo_bar, fooBar; x = 1; foo_bar = 2; input[fooBar]}`, - }}, - )} -} - -test_success_snake_cased_multiple_some_declaration if { - report(`p {some x, foo_bar; x = 5; input[foo_bar]}`) == set() -} - -test_fail_camel_cased_var_assignment if { - report(`allow { camelCase := 5 }`) == {object.union( - snake_case_violation, - {"location": {"col": 9, "file": "policy.rego", "row": 8, "text": `allow { camelCase := 5 }`}}, - )} -} - -test_fail_camel_cased_multiple_var_assignment if { - report(`allow { snake_case := "foo"; camelCase := 5 }`) == {object.union( - snake_case_violation, - {"location": { - "col": 30, "file": "policy.rego", "row": 8, - "text": `allow { snake_case := "foo"; camelCase := 5 }`, - }}, - )} -} - -test_success_snake_cased_var_assignment if { - report(`allow { snake_case := 5 }`) == set() -} - -test_fail_camel_cased_some_in_value if { - report(`allow { some cC in input }`) == {object.union( - snake_case_violation, - {"location": {"col": 14, "file": "policy.rego", "row": 8, "text": `allow { some cC in input }`}}, - )} -} - -test_fail_camel_cased_some_in_key_value if { - report(`allow { some cC, sc in input }`) == {object.union( - snake_case_violation, - {"location": {"col": 14, "file": "policy.rego", "row": 8, "text": `allow { some cC, sc in input }`}}, - )} -} - -test_fail_camel_cased_some_in_key_value_2 if { - report(`allow { some sc, cC in input }`) == {object.union( - snake_case_violation, - {"location": {"col": 18, "file": "policy.rego", "row": 8, "text": `allow { some sc, cC in input }`}}, - )} -} - -test_success_snake_cased_some_in if { - report(`allow { some sc in input }`) == set() -} - -test_fail_camel_cased_every_value if { - report(`allow { every cC in input { cC == 1 } }`) == {object.union( - snake_case_violation, - {"location": {"col": 15, "file": "policy.rego", "row": 8, "text": `allow { every cC in input { cC == 1 } }`}}, - )} -} - -test_fail_camel_cased_every_key if { - r := report(`allow { every cC, sc in input { cC == 1; sc == 2 } }`) - r == {object.union( - snake_case_violation, - {"location": { - "col": 15, "file": "policy.rego", "row": 8, - "text": `allow { every cC, sc in input { cC == 1; sc == 2 } }`, - }}, - )} -} - -test_success_snake_cased_every if { - report(`allow { every sc in input { sc == 1 } }`) == set() -} - -# Prefer in operator over iteration - -test_fail_use_in_operator_string_lhs if { - r := report(`allow { - "admin" == input.user.roles[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 13, "file": "policy.rego", "row": 9, "text": "\t\"admin\" == input.user.roles[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_number_lhs if { - r := report(`allow { - 1 == input.lucky_numbers[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 7, "file": "policy.rego", "row": 9, "text": "\t1 == input.lucky_numbers[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_array_lhs if { - r := report(`allow { - [1] == input.arrays[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 9, "file": "policy.rego", "row": 9, "text": "\t[1] == input.arrays[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_boolean_lhs if { - r := report(`allow { - true == input.booleans[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 10, "file": "policy.rego", "row": 9, "text": "\ttrue == input.booleans[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_object_lhs if { - r := report(`allow { - {"x": "y"} == input.objects[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 16, "file": "policy.rego", "row": 9, "text": "\t{\"x\": \"y\"} == input.objects[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_null_lhs if { - r := report(`allow { - null == input.objects[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 10, "file": "policy.rego", "row": 9, "text": "\tnull == input.objects[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_set_lhs if { - r := report(`allow { - {"foo"} == input.objects[_] - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 13, "file": "policy.rego", "row": 9, "text": "\t{\"foo\"} == input.objects[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_var_lhs if { - report(`allow { - admin == input.user.roles[_] - }`) == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 11, "file": "policy.rego", "row": 9, "text": "\tadmin == input.user.roles[_]"}, - "level": "error", - }} -} - -test_fail_use_in_operator_string_rhs if { - report(`allow { - input.user.roles[_] == "admin" - }`) == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 2, "file": "policy.rego", "row": 9, "text": "\tinput.user.roles[_] == \"admin\""}, - "level": "error", - }} -} - -test_fail_use_in_operator_var_rhs if { - r := report(`allow { - input.user.roles[_] == admin - }`) - r == {{ - "category": "style", - "description": "Use in to check for membership", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), - }], - "title": "use-in-operator", - "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tinput.user.roles[_] == admin"}, - "level": "error", - }} -} - -test_success_refs_both_sides if { - report(`allow { required_roles[_] == input.user.roles[_] }`) == set() -} - -test_success_uses_in_operator if { - report(`allow { "admin" in input.user.roles[_] }`) == set() -} - -# Line length - -test_fail_line_too_long if { - r := report(`allow { -foo == bar; bar == baz; [a, b, c, d, e, f] := [1, 2, 3, 4, 5, 6]; qux := [q | some q in input.nonsense] - }`) - r == {{ - "category": "style", - "description": "Line too long", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/line-length", "style"), - }], - "title": "line-length", - "location": { - "col": 103, "file": "policy.rego", "row": 9, - "text": `foo == bar; bar == baz; [a, b, c, d, e, f] := [1, 2, 3, 4, 5, 6]; qux := [q | some q in input.nonsense]`, - }, - "level": "error", - }} -} - -test_success_line_not_too_long if { - report(`allow { "foo" == "bar" }`) == set() -} - -test_fail_unification_in_default_assignment if { - report(`default x = false`) == {{ - "category": "style", - "description": "Prefer := over = for assignment", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-assignment-operator", "style"), - }], - "title": "use-assignment-operator", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "default x = false"}, - "level": "error", - }} -} - -test_success_assignment_in_default_assignment if { - report(`default x := false`) == set() -} - -test_fail_unification_in_object_rule_assignment if { - r := report(`x["a"] = 1`) - r == {{ - "category": "style", - "description": "Prefer := over = for assignment", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/use-assignment-operator", "style"), - }], - "title": "use-assignment-operator", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `x["a"] = 1`}, - "level": "error", - }} -} - -test_success_assignment_in_object_rule_assignment if { - report(`x["a"] := 1`) == set() -} - -# Some cases blocked by https://github.com/StyraInc/regal/issues/6 - e.g: -# -# allow = true { true } -# -# f(x) = 5 - -test_fail_todo_comment if { - report(`# TODO: do someting clever`) == {{ - "category": "style", - "description": "Avoid TODO comments", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/todo-comment", "style"), - }], - "title": "todo-comment", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `# TODO: do someting clever`}, - "level": "error", - }} -} - -test_fail_fixme_comment if { - report(`# fixme: this is broken`) == {{ - "category": "style", - "description": "Avoid TODO comments", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/todo-comment", "style"), - }], - "title": "todo-comment", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `# fixme: this is broken`}, - "level": "error", - }} -} - -test_success_no_todo_comment if { - report(`# This code is great`) == set() -} - -test_fail_function_references_input if { - report(`f(_) { input.foo }`) == {{ - "category": "style", - "description": "Reference to input, data or rule ref in function body", - "location": {"col": 8, "file": "policy.rego", "row": 8, "text": `f(_) { input.foo }`}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), - }], - "title": "external-reference", - "level": "error", - }} -} - -test_fail_function_references_data if { - report(`f(_) { data.foo }`) == {{ - "category": "style", - "description": "Reference to input, data or rule ref in function body", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), - }], - "title": "external-reference", - "location": {"col": 8, "file": "policy.rego", "row": 8, "text": `f(_) { data.foo }`}, - "level": "error", - }} -} - -test_fail_function_references_rule if { - r := report(` -foo := "bar" - -f(x, y) { - x == 5 - y == foo -} - `) - r == {{ - "category": "style", - "description": "Reference to input, data or rule ref in function body", - "location": {"col": 7, "file": "policy.rego", "row": 13, "text": ` y == foo`}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/external-reference", "style"), - }], - "title": "external-reference", - "level": "error", - }} -} - -test_success_function_references_no_input_or_data if { - report(`f(x) { x == true }`) == set() -} - -test_success_function_references_no_input_or_data_reverse if { - report(`f(x) { true == x }`) == set() -} - -test_success_function_references_only_own_vars if { - report(`f(x) { y := x; y == 10 }`) == set() -} - -test_success_function_references_only_own_vars_nested if { - report(`f(x, z) { y := x; y == [1, 2, z]}`) == set() -} - -test_fail_rule_name_starts_with_get if { - r := report(`get_foo := 1`) - r == {{ - "category": "style", - "description": "Avoid get_ and list_ prefix for rules and functions", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/avoid-get-and-list-prefix", "style"), - }], - "title": "avoid-get-and-list-prefix", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "get_foo := 1"}, - "level": "error", - }} -} - -test_fail_function_name_starts_with_list if { - r := report(`list_users(datasource) := ["we", "have", "no", "users"]`) - r == {{ - "category": "style", - "description": "Avoid get_ and list_ prefix for rules and functions", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/avoid-get-and-list-prefix", "style"), - }], - "title": "avoid-get-and-list-prefix", - "location": { - "col": 1, "file": "policy.rego", "row": 8, - "text": `list_users(datasource) := ["we", "have", "no", "users"]`, - }, - "level": "error", - }} -} - -test_fail_unconditional_assignment_in_body if { - r := report(`x := y { - y := 1 - }`) - r == {{ - "category": "style", - "description": "Unconditional assignment in rule body", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/unconditional-assignment", "style"), - }], - "title": "unconditional-assignment", - "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\ty := 1"}, - "level": "error", - }} -} - -test_fail_unconditional_eq_in_body if { - r := report(`x = y { - y = 1 - }`) - r == {{ - "category": "style", - "description": "Unconditional assignment in rule body", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/unconditional-assignment", "style"), - }], - "title": "unconditional-assignment", - "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\ty = 1"}, - "level": "error", - }} -} - -test_success_conditional_assignment_in_body if { - report(`x := y { input.foo == "bar"; y := 1 }`) == set() -} - -test_success_unconditional_assignment_but_with_in_body if { - report(`x := y { y := 5 with input as 1 }`) == set() -} - -test_fail_function_arg_return_value if { - r := report(`foo := i { indexof("foo", "o", i) }`) - r == {{ - "category": "style", - "description": "Function argument used for return value", - "level": "error", - "location": {"col": 32, "file": "policy.rego", "row": 8, "text": "foo := i { indexof(\"foo\", \"o\", i) }"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/function-arg-return", "style"), - }], - "title": "function-arg-return", - }} -} - -test_success_function_arg_return_value_except_function if { - r := style.report with input as ast.with_future_keywords(`foo := i { indexof("foo", "o", i) }`) - with config.for_rule as { - "level": "error", - "except-functions": ["indexof"], - } - r == set() -} - -report(snippet) := report if { - # regal ignore:external-reference - report := style.report with input as ast.with_future_keywords(snippet) - with config.for_rule as {"level": "error", "max-line-length": 80} -} diff --git a/bundle/regal/rules/style/todo_comment.rego b/bundle/regal/rules/style/todo_comment.rego new file mode 100644 index 00000000..09c13bef --- /dev/null +++ b/bundle/regal/rules/style/todo_comment.rego @@ -0,0 +1,35 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# For comments, OPA uses capital-cases Text and Location rather +# than text and location. As fixing this would potentially break +# things, we need to take it into consideration here. + +todo_identifiers := ["todo", "TODO", "fixme", "FIXME"] + +todo_pattern := sprintf(`^\s*(%s)`, [concat("|", todo_identifiers)]) + +# METADATA +# title: todo-comment +# description: Avoid TODO comments +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/todo-comment +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some comment in input.comments + text := base64.decode(comment.Text) + regex.match(todo_pattern, text) + + violation := result.fail(rego.metadata.rule(), result.location(comment)) +} diff --git a/bundle/regal/rules/style/todo_comment_test.rego b/bundle/regal/rules/style/todo_comment_test.rego new file mode 100644 index 00000000..d7007586 --- /dev/null +++ b/bundle/regal/rules/style/todo_comment_test.rego @@ -0,0 +1,40 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_todo_comment if { + report(`# TODO: do someting clever`) == {{ + "category": "style", + "description": "Avoid TODO comments", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/todo-comment", "style"), + }], + "title": "todo-comment", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `# TODO: do someting clever`}, + "level": "error", + }} +} + +test_fail_fixme_comment if { + report(`# fixme: this is broken`) == {{ + "category": "style", + "description": "Avoid TODO comments", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/todo-comment", "style"), + }], + "title": "todo-comment", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `# fixme: this is broken`}, + "level": "error", + }} +} + +test_success_no_todo_comment if { + report(`# This code is great`) == set() +} diff --git a/bundle/regal/rules/style/unconditional_assignment.rego b/bundle/regal/rules/style/unconditional_assignment.rego new file mode 100644 index 00000000..72009517 --- /dev/null +++ b/bundle/regal/rules/style/unconditional_assignment.rego @@ -0,0 +1,49 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: unconditional-assignment +# description: Unconditional assignment in rule body +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/unconditional-assignment +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + + # Single expression in rule body + # There's going to be a few cases where more expressions + # are in the body and it still "unconditional", like e.g + # a `print` call.. but let's keep it simple for now + count(rule.body) == 1 + + # Var assignment in rule head + rule.head.value.type == "var" + rule_head_var := rule.head.value.value + + # If a `with` statement is found in body, back out, as these + # can't be moved to the rule head + not rule.body[0]["with"] + + # Which is an assignment (= or :=) + terms := rule.body[0].terms + terms[0].type == "ref" + terms[0].value[0].type == "var" + terms[0].value[0].value in {"eq", "assign"} + + # Of var declared in rule head + terms[1].type == "var" + terms[1].value == rule_head_var + + violation := result.fail(rego.metadata.rule(), result.location(terms[1])) +} diff --git a/bundle/regal/rules/style/unconditional_assignment_test.rego b/bundle/regal/rules/style/unconditional_assignment_test.rego new file mode 100644 index 00000000..bb1cd5aa --- /dev/null +++ b/bundle/regal/rules/style/unconditional_assignment_test.rego @@ -0,0 +1,50 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_unconditional_assignment_in_body if { + r := report(`x := y { + y := 1 + }`) + r == {{ + "category": "style", + "description": "Unconditional assignment in rule body", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/unconditional-assignment", "style"), + }], + "title": "unconditional-assignment", + "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\ty := 1"}, + "level": "error", + }} +} + +test_fail_unconditional_eq_in_body if { + r := report(`x = y { + y = 1 + }`) + r == {{ + "category": "style", + "description": "Unconditional assignment in rule body", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/unconditional-assignment", "style"), + }], + "title": "unconditional-assignment", + "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\ty = 1"}, + "level": "error", + }} +} + +test_success_conditional_assignment_in_body if { + report(`x := y { input.foo == "bar"; y := 1 }`) == set() +} + +test_success_unconditional_assignment_but_with_in_body if { + report(`x := y { y := 5 with input as 1 }`) == set() +} diff --git a/bundle/regal/rules/style/use_assignment_operator.rego b/bundle/regal/rules/style/use_assignment_operator.rego new file mode 100644 index 00000000..695b47cf --- /dev/null +++ b/bundle/regal/rules/style/use_assignment_operator.rego @@ -0,0 +1,53 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result +import data.regal.util + +# Some cases blocked by https://github.com/StyraInc/regal/issues/6 - e.g: +# +# allow = true { true } +# +# f(x) = 5 + +# METADATA +# title: use-assignment-operator +# description: Prefer := over = for assignment +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/use-assignment-operator +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule["default"] == true + not rule.head.assign + + violation := result.fail(rego.metadata.rule(), result.location(rule)) +} + +# METADATA +# title: use-assignment-operator +# description: Prefer := over = for assignment +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/use-assignment-operator +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + rule.head.key + rule.head.value + not rule.head.assign + + violation := result.fail(rego.metadata.rule(), result.location(rule.head.ref[0])) +} diff --git a/bundle/regal/rules/style/use_assignment_operator_test.rego b/bundle/regal/rules/style/use_assignment_operator_test.rego new file mode 100644 index 00000000..bf2c6bf2 --- /dev/null +++ b/bundle/regal/rules/style/use_assignment_operator_test.rego @@ -0,0 +1,45 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_unification_in_default_assignment if { + report(`default x = false`) == {{ + "category": "style", + "description": "Prefer := over = for assignment", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-assignment-operator", "style"), + }], + "title": "use-assignment-operator", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": "default x = false"}, + "level": "error", + }} +} + +test_success_assignment_in_default_assignment if { + report(`default x := false`) == set() +} + +test_fail_unification_in_object_rule_assignment if { + r := report(`x["a"] = 1`) + r == {{ + "category": "style", + "description": "Prefer := over = for assignment", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-assignment-operator", "style"), + }], + "title": "use-assignment-operator", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `x["a"] = 1`}, + "level": "error", + }} +} + +test_success_assignment_in_object_rule_assignment if { + report(`x["a"] := 1`) == set() +} diff --git a/bundle/regal/rules/style/use_in_operator.rego b/bundle/regal/rules/style/use_in_operator.rego new file mode 100644 index 00000000..8dfc6872 --- /dev/null +++ b/bundle/regal/rules/style/use_in_operator.rego @@ -0,0 +1,67 @@ +package regal.rules.style + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.result + +# METADATA +# title: use-in-operator +# description: Use in to check for membership +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/use-in-operator +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some expr in eq_exprs + + expr.terms[1].type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} + expr.terms[2].type == "ref" + + last := regal.last(expr.terms[2].value) + + last.type == "var" + startswith(last.value, "$") + + violation := result.fail(rego.metadata.rule(), result.location(expr.terms[2].value[0])) +} + +# METADATA +# title: use-in-operator +# description: Use in to check for membership +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/use-in-operator +# custom: +# category: style +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some expr in eq_exprs + + expr.terms[1].type == "ref" + expr.terms[2].type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} + + last := regal.last(expr.terms[1].value) + + last.type == "var" + startswith(last.value, "$") + + violation := result.fail(rego.metadata.rule(), result.location(expr.terms[1].value[0])) +} + +eq_exprs contains expr if { + config.for_rule({"category": "style", "title": "use-in-operator"}).level != "ignore" + + some rule in input.rules + some expr in rule.body + + expr.terms[0].type == "ref" + expr.terms[0].value[0].type == "var" + expr.terms[0].value[0].value == "equal" +} diff --git a/bundle/regal/rules/style/use_in_operator_test.rego b/bundle/regal/rules/style/use_in_operator_test.rego new file mode 100644 index 00000000..12b1acab --- /dev/null +++ b/bundle/regal/rules/style/use_in_operator_test.rego @@ -0,0 +1,184 @@ +package regal.rules.style_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.style +import data.regal.rules.style.common_test.report + +test_fail_use_in_operator_string_lhs if { + r := report(`allow { + "admin" == input.user.roles[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 13, "file": "policy.rego", "row": 9, "text": "\t\"admin\" == input.user.roles[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_number_lhs if { + r := report(`allow { + 1 == input.lucky_numbers[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 7, "file": "policy.rego", "row": 9, "text": "\t1 == input.lucky_numbers[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_array_lhs if { + r := report(`allow { + [1] == input.arrays[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 9, "file": "policy.rego", "row": 9, "text": "\t[1] == input.arrays[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_boolean_lhs if { + r := report(`allow { + true == input.booleans[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 10, "file": "policy.rego", "row": 9, "text": "\ttrue == input.booleans[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_object_lhs if { + r := report(`allow { + {"x": "y"} == input.objects[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 16, "file": "policy.rego", "row": 9, "text": "\t{\"x\": \"y\"} == input.objects[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_null_lhs if { + r := report(`allow { + null == input.objects[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 10, "file": "policy.rego", "row": 9, "text": "\tnull == input.objects[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_set_lhs if { + r := report(`allow { + {"foo"} == input.objects[_] + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 13, "file": "policy.rego", "row": 9, "text": "\t{\"foo\"} == input.objects[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_var_lhs if { + report(`allow { + admin == input.user.roles[_] + }`) == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 11, "file": "policy.rego", "row": 9, "text": "\tadmin == input.user.roles[_]"}, + "level": "error", + }} +} + +test_fail_use_in_operator_string_rhs if { + report(`allow { + input.user.roles[_] == "admin" + }`) == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 2, "file": "policy.rego", "row": 9, "text": "\tinput.user.roles[_] == \"admin\""}, + "level": "error", + }} +} + +test_fail_use_in_operator_var_rhs if { + r := report(`allow { + input.user.roles[_] == admin + }`) + r == {{ + "category": "style", + "description": "Use in to check for membership", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/use-in-operator", "style"), + }], + "title": "use-in-operator", + "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tinput.user.roles[_] == admin"}, + "level": "error", + }} +} + +test_success_refs_both_sides if { + report(`allow { required_roles[_] == input.user.roles[_] }`) == set() +} + +test_success_uses_in_operator if { + report(`allow { "admin" in input.user.roles[_] }`) == set() +} diff --git a/bundle/regal/rules/testing/common_test.rego b/bundle/regal/rules/testing/common_test.rego new file mode 100644 index 00000000..aaf392b0 --- /dev/null +++ b/bundle/regal/rules/testing/common_test.rego @@ -0,0 +1,13 @@ +package regal.rules.testing.common_test + +import future.keywords.if + +import data.regal.ast +import data.regal.config +import data.regal.rules.testing + +report(snippet) := report if { + # regal ignore:external-reference + report := testing.report with input as ast.with_future_keywords(snippet) + with config.for_rule as {"level": "error"} +} diff --git a/bundle/regal/rules/testing/file_missing_test_suffix.rego b/bundle/regal/rules/testing/file_missing_test_suffix.rego new file mode 100644 index 00000000..29ffa0d6 --- /dev/null +++ b/bundle/regal/rules/testing/file_missing_test_suffix.rego @@ -0,0 +1,27 @@ +package regal.rules.testing + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: file-missing-test-suffix +# description: Files containing tests should have a _test.rego suffix +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/file-missing-test-suffix +# custom: +# category: testing +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + count(ast.tests) > 0 + + not endswith(input.regal.file.name, "_test.rego") + + violation := result.fail(rego.metadata.rule(), {"location": {"file": input.regal.file.name}}) +} diff --git a/bundle/regal/rules/testing/file_missing_test_suffix_test.rego b/bundle/regal/rules/testing/file_missing_test_suffix_test.rego new file mode 100644 index 00000000..17ac16f4 --- /dev/null +++ b/bundle/regal/rules/testing/file_missing_test_suffix_test.rego @@ -0,0 +1,28 @@ +package regal.rules.testing_test + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.rules.testing + +test_fail_test_in_file_without_test_suffix if { + ast := regal.parse_module("policy.rego", `package foo_test + + test_foo { false } + `) + + r := testing.report with input as ast with config.for_rule as {"level": "error"} + r == {{ + "category": "testing", + "description": "Files containing tests should have a _test.rego suffix", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/file-missing-test-suffix", "testing"), + }], + "title": "file-missing-test-suffix", + "location": {"file": "policy.rego"}, + "level": "error", + }} +} diff --git a/bundle/regal/rules/testing/identically_named_tests.rego b/bundle/regal/rules/testing/identically_named_tests.rego new file mode 100644 index 00000000..e6324076 --- /dev/null +++ b/bundle/regal/rules/testing/identically_named_tests.rego @@ -0,0 +1,31 @@ +package regal.rules.testing + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: identically-named-tests +# description: Multiple tests with same name +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/identically-named-tests +# custom: +# category: testing +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + test_names := [rule.head.name | some rule in ast.tests] + + some i, name in test_names + + name in array.slice(test_names, 0, i) + + # We don't currently have location for rule heads, but this should + # change soon: https://github.com/open-policy-agent/opa/pull/5811 + violation := result.fail(rego.metadata.rule(), {"location": {"file": input.regal.file.name}}) +} diff --git a/bundle/regal/rules/testing/identically_named_tests_test.rego b/bundle/regal/rules/testing/identically_named_tests_test.rego new file mode 100644 index 00000000..93f3888c --- /dev/null +++ b/bundle/regal/rules/testing/identically_named_tests_test.rego @@ -0,0 +1,41 @@ +package regal.rules.testing_test + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.rules.testing + +test_fail_identically_named_tests if { + ast := regal.parse_module("foo_test.rego", ` + package foo_test + + test_foo { false } + test_foo { true } + `) + result := testing.report with input as ast + result == {{ + "category": "testing", + "description": "Multiple tests with same name", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/identically-named-tests", "testing"), + }], + "title": "identically-named-tests", + "location": {"file": "foo_test.rego"}, + "level": "error", + }} +} + +test_success_differently_named_tests if { + ast := regal.parse_module("foo_test.rego", ` + package foo_test + + test_foo { false } + test_bar { true } + test_baz { 1 == 1 } + `) + result := testing.report with input as ast + result == set() +} diff --git a/bundle/regal/rules/testing/print_or_trace_call.rego b/bundle/regal/rules/testing/print_or_trace_call.rego new file mode 100644 index 00000000..34ad0c47 --- /dev/null +++ b/bundle/regal/rules/testing/print_or_trace_call.rego @@ -0,0 +1,28 @@ +package regal.rules.testing + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: print-or-trace-call +# description: Call to print or trace function +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/print-or-trace-call +# custom: +# category: testing +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some call in ast.find_builtin_calls(input) + + name := call[0].value[0].value + name in {"print", "trace"} + + violation := result.fail(rego.metadata.rule(), result.location(call[0].value[0])) +} diff --git a/bundle/regal/rules/testing/print_or_trace_call_test.rego b/bundle/regal/rules/testing/print_or_trace_call_test.rego new file mode 100644 index 00000000..67ec69df --- /dev/null +++ b/bundle/regal/rules/testing/print_or_trace_call_test.rego @@ -0,0 +1,40 @@ +package regal.rules.testing_test + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.rules.testing.common_test.report + +test_fail_call_to_print_and_trace if { + r := report(`allow { + print("foo") + + x := [i | i = 0; trace("bar")] + }`) + r == { + { + "category": "testing", + "description": "Call to print or trace function", + "level": "error", + "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tprint(\"foo\")"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/print-or-trace-call", "testing"), + }], + "title": "print-or-trace-call", + }, + { + "category": "testing", + "description": "Call to print or trace function", + "level": "error", + "location": {"col": 20, "file": "policy.rego", "row": 11, "text": "\t\tx := [i | i = 0; trace(\"bar\")]"}, + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/print-or-trace-call", "testing"), + }], + "title": "print-or-trace-call", + }, + } +} diff --git a/bundle/regal/rules/testing/test_outside_test_package.rego b/bundle/regal/rules/testing/test_outside_test_package.rego new file mode 100644 index 00000000..60a609a7 --- /dev/null +++ b/bundle/regal/rules/testing/test_outside_test_package.rego @@ -0,0 +1,27 @@ +package regal.rules.testing + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: test-outside-test-package +# description: Test outside of test package +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/test-outside-test-package +# custom: +# category: testing +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + not endswith(ast.package_name, "_test") + + some rule in ast.tests + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} diff --git a/bundle/regal/rules/testing/test_outside_test_package_test.rego b/bundle/regal/rules/testing/test_outside_test_package_test.rego new file mode 100644 index 00000000..cf34fb5c --- /dev/null +++ b/bundle/regal/rules/testing/test_outside_test_package_test.rego @@ -0,0 +1,37 @@ +package regal.rules.testing_test + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.rules.testing + +test_fail_test_outside_test_package if { + r := testing.report with input as ast.with_future_keywords(`test_foo { false }`) + with config.for_rule as {"level": "error"} + with input.regal.file.name as "p_test.rego" + + r == {{ + "category": "testing", + "description": "Test outside of test package", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/test-outside-test-package", "testing"), + }], + "title": "test-outside-test-package", + "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `test_foo { false }`}, + "level": "error", + }} +} + +test_success_test_inside_test_package if { + ast := regal.parse_module("foo_test.rego", ` + package foo_test + + test_foo { false } + `) + result := testing.report with input as ast + result == set() +} diff --git a/bundle/regal/rules/testing/testing.rego b/bundle/regal/rules/testing/testing.rego deleted file mode 100644 index 61664ab9..00000000 --- a/bundle/regal/rules/testing/testing.rego +++ /dev/null @@ -1,113 +0,0 @@ -package regal.rules.testing - -import future.keywords.contains -import future.keywords.if -import future.keywords.in - -import data.regal.ast -import data.regal.config -import data.regal.result - -package_name := concat(".", [path.value | some path in input["package"].path]) - -tests := [rule | - some rule in input.rules - startswith(rule.head.name, "test_") -] - -# METADATA -# title: test-outside-test-package -# description: Test outside of test package -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/test-outside-test-package -# custom: -# category: testing -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - not endswith(package_name, "_test") - - some rule in tests - - violation := result.fail(rego.metadata.rule(), result.location(rule.head)) -} - -# METADATA -# title: file-missing-test-suffix -# description: Files containing tests should have a _test.rego suffix -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/file-missing-test-suffix -# custom: -# category: testing -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - count(tests) > 0 - - not endswith(input.regal.file.name, "_test.rego") - - violation := result.fail(rego.metadata.rule(), {"location": {"file": input.regal.file.name}}) -} - -# METADATA -# title: identically-named-tests -# description: Multiple tests with same name -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/identically-named-tests -# custom: -# category: testing -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - test_names := [rule.head.name | some rule in tests] - - some i, name in test_names - - name in array.slice(test_names, 0, i) - - # We don't currently have location for rule heads, but this should - # change soon: https://github.com/open-policy-agent/opa/pull/5811 - violation := result.fail(rego.metadata.rule(), {"location": {"file": input.regal.file.name}}) -} - -# METADATA -# title: todo-test -# description: TODO test encountered -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/todo-test -# custom: -# category: testing -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some rule in input.rules - - startswith(rule.head.name, "todo_test_") - - # We don't currently have location for rule heads, but this should - # change soon: https://github.com/open-policy-agent/opa/pull/5811 - violation := result.fail(rego.metadata.rule(), {"location": {"file": input.regal.file.name}}) -} - -# METADATA -# title: print-or-trace-call -# description: Call to print or trace function -# related_resources: -# - description: documentation -# ref: $baseUrl/$category/print-or-trace-call -# custom: -# category: testing -report contains violation if { - config.for_rule(rego.metadata.rule()).level != "ignore" - - some call in ast.find_builtin_calls(input) - - name := call[0].value[0].value - name in {"print", "trace"} - - violation := result.fail(rego.metadata.rule(), result.location(call[0].value[0])) -} diff --git a/bundle/regal/rules/testing/testing_test.rego b/bundle/regal/rules/testing/testing_test.rego deleted file mode 100644 index 4c149458..00000000 --- a/bundle/regal/rules/testing/testing_test.rego +++ /dev/null @@ -1,144 +0,0 @@ -package regal.rules.testing_test - -import future.keywords.if - -import data.regal.ast -import data.regal.config -import data.regal.rules.testing - -test_fail_test_outside_test_package if { - report(`test_foo { false }`) with input.regal.file.name as "p_test.rego" == {{ - "category": "testing", - "description": "Test outside of test package", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/test-outside-test-package", "testing"), - }], - "title": "test-outside-test-package", - "location": {"col": 1, "file": "policy.rego", "row": 8, "text": `test_foo { false }`}, - "level": "error", - }} -} - -test_success_test_inside_test_package if { - ast := regal.parse_module("foo_test.rego", ` - package foo_test - - test_foo { false } - `) - result := testing.report with input as ast - result == set() -} - -test_fail_test_in_file_without_test_suffix if { - ast := regal.parse_module("policy.rego", `package foo_test - - test_foo { false } - `) - - r := testing.report with input as ast with config.for_rule as {"level": "error"} - - r == {{ - "category": "testing", - "description": "Files containing tests should have a _test.rego suffix", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/file-missing-test-suffix", "testing"), - }], - "title": "file-missing-test-suffix", - "location": {"file": "policy.rego"}, - "level": "error", - }} -} - -test_fail_identically_named_tests if { - ast := regal.parse_module("foo_test.rego", ` - package foo_test - - test_foo { false } - test_foo { true } - `) - result := testing.report with input as ast - result == {{ - "category": "testing", - "description": "Multiple tests with same name", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/identically-named-tests", "testing"), - }], - "title": "identically-named-tests", - "location": {"file": "foo_test.rego"}, - "level": "error", - }} -} - -test_success_differently_named_tests if { - ast := regal.parse_module("foo_test.rego", ` - package foo_test - - test_foo { false } - test_bar { true } - test_baz { 1 == 1 } - `) - result := testing.report with input as ast - result == set() -} - -test_fail_todo_test if { - ast := regal.parse_module("foo_test.rego", ` - package foo_test - - todo_test_foo { false } - - test_bar { true } - `) - result := testing.report with input as ast - result == {{ - "category": "testing", - "description": "TODO test encountered", - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/todo-test", "testing"), - }], - "title": "todo-test", - "location": {"file": "foo_test.rego"}, - "level": "error", - }} -} - -test_fail_call_to_print_and_trace if { - r := report(`allow { - print("foo") - - x := [i | i = 0; trace("bar")] - }`) - r == { - { - "category": "testing", - "description": "Call to print or trace function", - "level": "error", - "location": {"col": 3, "file": "policy.rego", "row": 9, "text": "\t\tprint(\"foo\")"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/print-or-trace-call", "testing"), - }], - "title": "print-or-trace-call", - }, - { - "category": "testing", - "description": "Call to print or trace function", - "level": "error", - "location": {"col": 20, "file": "policy.rego", "row": 11, "text": "\t\tx := [i | i = 0; trace(\"bar\")]"}, - "related_resources": [{ - "description": "documentation", - "ref": config.docs.resolve_url("$baseUrl/$category/print-or-trace-call", "testing"), - }], - "title": "print-or-trace-call", - }, - } -} - -report(snippet) := report if { - # regal ignore:external-reference - report := testing.report with input as ast.with_future_keywords(snippet) -} diff --git a/bundle/regal/rules/testing/todo_test.rego b/bundle/regal/rules/testing/todo_test.rego new file mode 100644 index 00000000..cc7ad5a7 --- /dev/null +++ b/bundle/regal/rules/testing/todo_test.rego @@ -0,0 +1,27 @@ +package regal.rules.testing + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.ast +import data.regal.config +import data.regal.result + +# METADATA +# title: todo-test +# description: TODO test encountered +# related_resources: +# - description: documentation +# ref: $baseUrl/$category/todo-test +# custom: +# category: testing +report contains violation if { + config.for_rule(rego.metadata.rule()).level != "ignore" + + some rule in input.rules + + startswith(rule.head.name, "todo_test_") + + violation := result.fail(rego.metadata.rule(), result.location(rule.head)) +} diff --git a/bundle/regal/rules/testing/todo_test_test.rego b/bundle/regal/rules/testing/todo_test_test.rego new file mode 100644 index 00000000..5f8c6ebe --- /dev/null +++ b/bundle/regal/rules/testing/todo_test_test.rego @@ -0,0 +1,30 @@ +package regal.rules.testing_test + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +import data.regal.config +import data.regal.rules.testing + +test_fail_todo_test if { + ast := regal.parse_module("foo_test.rego", ` + package foo_test + + todo_test_foo { false } + + test_bar { true } + `) + r := testing.report with input as ast + r == {{ + "category": "testing", + "description": "TODO test encountered", + "related_resources": [{ + "description": "documentation", + "ref": config.docs.resolve_url("$baseUrl/$category/todo-test", "testing"), + }], + "title": "todo-test", + "location": {"col": 2, "file": "foo_test.rego", "row": 4, "text": "\ttodo_test_foo { false }"}, + "level": "error", + }} +} diff --git a/p.rego b/p.rego new file mode 100644 index 00000000..c89cd18d --- /dev/null +++ b/p.rego @@ -0,0 +1 @@ +package p