├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build.yaml │ ├── codeql.yml │ ├── push-tags.yaml │ ├── scorecard.yml │ ├── update-caps.yml │ ├── update-docs.yaml │ └── update-example-index.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .pre-commit-hooks.yaml ├── .regal └── config.yaml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── build ├── capabilities.json ├── do.rq ├── download-and-run.sh ├── dprint.json ├── package-lock.json ├── package.json ├── simplecov │ └── simplecov.rego ├── update-readme.sh └── workflows │ └── update_example_index.rego ├── bundle ├── .manifest ├── bundle.go └── regal │ ├── ast │ ├── ast.rego │ ├── ast_test.rego │ ├── comments.rego │ ├── imports.rego │ ├── imports_test.rego │ ├── keywords.rego │ ├── keywords_test.rego │ ├── search.rego │ ├── search_test.rego │ └── testing.rego │ ├── capabilities │ └── capabilities.rego │ ├── config │ ├── config.rego │ ├── config_test.rego │ ├── exclusion.rego │ ├── exclusion_test.rego │ └── provided │ │ └── data.yaml │ ├── lsp │ ├── codelens │ │ ├── codelens.rego │ │ └── codelens_test.rego │ ├── completion │ │ ├── kind │ │ │ ├── kind.rego │ │ │ └── kind_test.rego │ │ ├── location │ │ │ ├── location.rego │ │ │ └── location_test.rego │ │ ├── main.rego │ │ ├── main_test.rego │ │ ├── providers │ │ │ ├── booleans │ │ │ │ ├── booleans.rego │ │ │ │ └── booleans_test.rego │ │ │ ├── commonrule │ │ │ │ ├── commonrule.rego │ │ │ │ └── commonrule_test.rego │ │ │ ├── default │ │ │ │ ├── default.rego │ │ │ │ └── default_test.rego │ │ │ ├── import │ │ │ │ ├── import.rego │ │ │ │ └── import_test.rego │ │ │ ├── input │ │ │ │ ├── input.rego │ │ │ │ └── input_test.rego │ │ │ ├── inputdotjson │ │ │ │ ├── inputdotjson.rego │ │ │ │ └── inputdotjson_test.rego │ │ │ ├── locals │ │ │ │ ├── locals.rego │ │ │ │ └── locals_test.rego │ │ │ ├── package │ │ │ │ ├── package.rego │ │ │ │ └── package_test.rego │ │ │ ├── packagename │ │ │ │ ├── packagename.rego │ │ │ │ └── packagename_test.rego │ │ │ ├── regov1 │ │ │ │ ├── regov1.rego │ │ │ │ └── regov1_test.rego │ │ │ ├── rulerefs │ │ │ │ ├── rulerefs.rego │ │ │ │ └── rulerefs_test.rego │ │ │ ├── snippet │ │ │ │ ├── snippet.rego │ │ │ │ └── snippet_test.rego │ │ │ └── test_utils │ │ │ │ └── test_utils.rego │ │ ├── ref_names.rego │ │ └── ref_names_test.rego │ └── util │ │ └── location │ │ └── location.rego │ ├── main │ ├── main.rego │ └── main_test.rego │ ├── regal.rego │ ├── result │ ├── result.rego │ └── result_test.rego │ ├── rules │ ├── bugs │ │ ├── annotation-without-metadata │ │ │ ├── annotation_without_metadata.rego │ │ │ └── annotation_without_metadata_test.rego │ │ ├── argument-always-wildcard │ │ │ ├── argument_always_wildcard.rego │ │ │ └── argument_always_wildcard_test.rego │ │ ├── constant-condition │ │ │ ├── constant_condition.rego │ │ │ └── constant_condition_test.rego │ │ ├── deprecated-builtin │ │ │ ├── deprecated_builtin.rego │ │ │ └── deprecated_builtin_test.rego │ │ ├── duplicate-rule │ │ │ ├── duplicate_rule.rego │ │ │ └── duplicate_rule_test.rego │ │ ├── if-empty-object │ │ │ ├── if_empty_object.rego │ │ │ └── if_empty_object_test.rego │ │ ├── if-object-literal │ │ │ ├── if_object_literal.rego │ │ │ └── if_object_literal_test.rego │ │ ├── import-shadows-rule │ │ │ ├── import_shadows_rule.rego │ │ │ └── import_shadows_rule_test.rego │ │ ├── impossible-not │ │ │ ├── impossible_not.rego │ │ │ └── impossible_not_test.rego │ │ ├── inconsistent-args │ │ │ ├── inconsistent_args.rego │ │ │ └── inconsistent_args_test.rego │ │ ├── internal-entrypoint │ │ │ ├── internal_entrypoint.rego │ │ │ └── internal_entrypoint_test.rego │ │ ├── invalid-metadata-attribute │ │ │ ├── invalid_metadata_attribute.rego │ │ │ └── invalid_metadata_attribute_test.rego │ │ ├── leaked-internal-reference │ │ │ ├── leaked_internal_reference.rego │ │ │ └── leaked_internal_reference_test.rego │ │ ├── not-equals-in-loop │ │ │ ├── not_equals_in_loop.rego │ │ │ └── not_equals_in_loop_test.rego │ │ ├── redundant-existence-check │ │ │ ├── redundant_existence_check.rego │ │ │ └── redundant_existence_check_test.rego │ │ ├── redundant-loop-count │ │ │ ├── redundant_loop_count.rego │ │ │ └── redundant_loop_count_test.rego │ │ ├── rule-assigns-default │ │ │ ├── rule_assigns_default.rego │ │ │ └── rule_assigns_default_test.rego │ │ ├── rule-named-if │ │ │ ├── rule_named_if.rego │ │ │ └── rule_named_if_test.rego │ │ ├── rule-shadows-builtin │ │ │ ├── rule_shadows_builtin.rego │ │ │ └── rule_shadows_builtin_test.rego │ │ ├── sprintf-arguments-mismatch │ │ │ ├── sprintf_arguments_mismatch.rego │ │ │ └── sprintf_arguments_mismatch_test.rego │ │ ├── time-now-ns-twice │ │ │ ├── time_now_ns_twice.rego │ │ │ └── time_now_ns_twice_test.rego │ │ ├── top-level-iteration │ │ │ ├── top_level_iteration.rego │ │ │ └── top_level_iteration_test.rego │ │ ├── unassigned-return-value │ │ │ ├── unassigned_return_value.rego │ │ │ └── unassigned_return_value_test.rego │ │ ├── unused-output-variable │ │ │ ├── unused_output_variable.rego │ │ │ └── unused_output_variable_test.rego │ │ ├── var-shadows-builtin │ │ │ ├── var_shadows_builtin.rego │ │ │ └── var_shadows_builtin_test.rego │ │ └── zero-arity-function │ │ │ ├── zero_arity_function.rego │ │ │ └── zero_arity_function_test.rego │ ├── custom │ │ ├── forbidden-function-call │ │ │ ├── forbidden_function_call.rego │ │ │ └── forbidden_function_call_test.rego │ │ ├── missing-metadata │ │ │ ├── missing_metadata.rego │ │ │ └── missing_metadata_test.rego │ │ ├── naming-convention │ │ │ ├── naming_convention.rego │ │ │ └── naming_convention_test.rego │ │ ├── narrow-argument │ │ │ ├── narrow_argument.rego │ │ │ └── narrow_argument_test.rego │ │ ├── one-liner-rule │ │ │ ├── one_liner_rule.rego │ │ │ └── one_liner_rule_test.rego │ │ └── prefer-value-in-head │ │ │ ├── prefer_value_in_head.rego │ │ │ └── prefer_value_in_head_test.rego │ ├── idiomatic │ │ ├── ambiguous-scope │ │ │ ├── ambiguous_scope.rego │ │ │ └── ambiguous_scope_test.rego │ │ ├── boolean-assignment │ │ │ ├── boolean_assignment.rego │ │ │ └── boolean_assignment_test.rego │ │ ├── custom-has-key-construct │ │ │ ├── custom_has_key_construct.rego │ │ │ └── custom_has_key_construct_test.rego │ │ ├── custom-in-construct │ │ │ ├── custom_in_construct.rego │ │ │ └── custom_in_construct_test.rego │ │ ├── directory-package-mismatch │ │ │ ├── directory_package_mismatch.rego │ │ │ └── directory_package_mismatch_test.rego │ │ ├── equals-pattern-matching │ │ │ ├── equals_pattern_matching.rego │ │ │ └── equals_pattern_matching_test.rego │ │ ├── in-wildcard-key │ │ │ ├── in_wildcard_key.rego │ │ │ └── in_wildcard_key_test.rego │ │ ├── no-defined-entrypoint │ │ │ ├── no_defined_entrypoint.rego │ │ │ └── no_defined_entrypoint_test.rego │ │ ├── non-raw-regex-pattern │ │ │ ├── non_raw_regex_pattern.rego │ │ │ └── non_raw_regex_pattern_test.rego │ │ ├── prefer-set-or-object-rule │ │ │ ├── prefer_set_or_object_rule.rego │ │ │ └── prefer_set_or_object_rule_test.rego │ │ ├── single-item-in │ │ │ ├── single_item_in.rego │ │ │ └── single_item_in_test.rego │ │ ├── use-contains │ │ │ ├── use_contains.rego │ │ │ └── use_contains_test.rego │ │ ├── use-if │ │ │ ├── use_if.rego │ │ │ └── use_if_test.rego │ │ ├── use-in-operator │ │ │ ├── use_in_operator.rego │ │ │ └── use_in_operator_test.rego │ │ ├── use-object-keys │ │ │ ├── use_object_keys.rego │ │ │ └── use_object_keys_test.rego │ │ ├── use-some-for-output-vars │ │ │ ├── use_some_for_output_vars.rego │ │ │ └── use_some_for_output_vars_test.rego │ │ └── use-strings-count │ │ │ ├── use_strings_count.rego │ │ │ └── use_strings_count_test.rego │ ├── imports │ │ ├── avoid-importing-input │ │ │ ├── avoid_importing_input.rego │ │ │ └── avoid_importing_input_test.rego │ │ ├── circular-import │ │ │ ├── circular_import.rego │ │ │ └── circular_import_test.rego │ │ ├── confusing-alias │ │ │ ├── confusing_alias.rego │ │ │ └── confusing_alias_test.rego │ │ ├── ignored-import │ │ │ ├── ignored_import.rego │ │ │ └── ignored_import_test.rego │ │ ├── implicit-future-keywords │ │ │ ├── implicit_future_keywords.rego │ │ │ └── implicit_future_keywords_test.rego │ │ ├── import-after-rule │ │ │ ├── import_after_rule.rego │ │ │ └── import_after_rule_test.rego │ │ ├── import-shadows-builtin │ │ │ ├── import_shadows_builtin.rego │ │ │ └── import_shadows_builtin_test.rego │ │ ├── import-shadows-import │ │ │ ├── import_shadows_import.rego │ │ │ └── import_shadows_import_test.rego │ │ ├── pointless-import │ │ │ ├── pointless_import.rego │ │ │ └── pointless_import_test.rego │ │ ├── prefer-package-imports │ │ │ ├── prefer_package_imports.rego │ │ │ └── prefer_package_imports_test.rego │ │ ├── redundant-alias │ │ │ ├── redundant_alias.rego │ │ │ └── redundant_alias_test.rego │ │ ├── redundant-data-import │ │ │ ├── redundant_data_import.rego │ │ │ └── redundant_data_import_test.rego │ │ ├── unresolved-import │ │ │ ├── unresolved_import.rego │ │ │ └── unresolved_import_test.rego │ │ ├── unresolved-reference │ │ │ ├── unresolved_reference.rego │ │ │ └── unresolved_reference_test.rego │ │ └── use-rego-v1 │ │ │ ├── use_rego_v1.rego │ │ │ └── use_rego_v1_test.rego │ ├── performance │ │ ├── defer-assignment │ │ │ ├── defer_assignment.rego │ │ │ └── defer_assignment_test.rego │ │ ├── non-loop-expression │ │ │ ├── non_loop_expression.rego │ │ │ └── non_loop_expression_test.rego │ │ ├── walk-no-path │ │ │ ├── walk_no_path.rego │ │ │ └── walk_no_path_test.rego │ │ └── with-outside-test-context │ │ │ ├── with_outside_test_context.rego │ │ │ └── with_outside_test_context_test.rego │ ├── style │ │ ├── avoid-get-and-list-prefix │ │ │ ├── avoid_get_and_list_prefix.rego │ │ │ └── avoid_get_and_list_prefix_test.rego │ │ ├── chained-rule-body │ │ │ ├── chained_rule_body.rego │ │ │ └── chained_rule_body_test.rego │ │ ├── comprehension-term-assignment │ │ │ ├── comprehension_term_assignment.rego │ │ │ └── comprehension_term_assignment_test.rego │ │ ├── default-over-else │ │ │ ├── default_over_else.rego │ │ │ └── default_over_else_test.rego │ │ ├── default-over-not │ │ │ ├── default_over_not.rego │ │ │ └── default_over_not_test.rego │ │ ├── detached-metadata │ │ │ ├── detached_metadata.rego │ │ │ └── detached_metadata_test.rego │ │ ├── double-negative │ │ │ ├── double_negative.rego │ │ │ └── double_negative_test.rego │ │ ├── external-reference │ │ │ ├── external_reference.rego │ │ │ └── external_reference_test.rego │ │ ├── file-length │ │ │ ├── file_length.rego │ │ │ └── file_length_test.rego │ │ ├── function-arg-return │ │ │ ├── function_arg_return.rego │ │ │ └── function_arg_return_test.rego │ │ ├── line-length │ │ │ ├── line_length.rego │ │ │ └── line_length_test.rego │ │ ├── messy-rule │ │ │ ├── messy_rule.rego │ │ │ └── messy_rule_test.rego │ │ ├── mixed-iteration │ │ │ ├── mixed_iteration.rego │ │ │ └── mixed_iteration_test.rego │ │ ├── no-whitespace-comment │ │ │ ├── no_whitespace_comment.rego │ │ │ └── no_whitespace_comment_test.rego │ │ ├── opa-fmt │ │ │ ├── opa_fmt.rego │ │ │ └── opa_fmt_test.rego │ │ ├── pointless-reassignment │ │ │ ├── pointless_reassignment.rego │ │ │ └── pointless_reassignment_test.rego │ │ ├── prefer-snake-case │ │ │ ├── prefer_snake_case.rego │ │ │ └── prefer_snake_case_test.rego │ │ ├── prefer-some-in-iteration │ │ │ ├── prefer_some_in_iteration.rego │ │ │ └── prefer_some_in_iteration_test.rego │ │ ├── rule-length │ │ │ ├── rule_length.rego │ │ │ └── rule_length_test.rego │ │ ├── rule-name-repeats-package │ │ │ ├── rule_name_repeats_package.rego │ │ │ └── rule_name_repeats_package_test.rego │ │ ├── todo-comment │ │ │ ├── todo_comment.rego │ │ │ └── todo_comment_test.rego │ │ ├── trailing-default-rule │ │ │ ├── trailing_default_rule.rego │ │ │ └── trailing_default_rule_test.rego │ │ ├── unconditional-assignment │ │ │ ├── unconditional_assignment.rego │ │ │ └── unconditional_assignment_test.rego │ │ ├── unnecessary-some │ │ │ ├── unnecessary_some.rego │ │ │ └── unnecessary_some_test.rego │ │ ├── use-assignment-operator │ │ │ ├── use_assignment_operator.rego │ │ │ └── use_assignment_operator_test.rego │ │ └── yoda-condition │ │ │ ├── yoda_condition.rego │ │ │ └── yoda_condition_test.rego │ └── testing │ │ ├── dubious-print-sprintf │ │ ├── dubious_print_sprintf.rego │ │ └── dubious_print_sprintf_test.rego │ │ ├── file-missing-test-suffix │ │ ├── file_missing_test_suffix.rego │ │ └── file_missing_test_suffix_test.rego │ │ ├── identically-named-tests │ │ ├── identically_named_tests.rego │ │ └── identically_named_tests_test.rego │ │ ├── metasyntactic-variable │ │ ├── metasyntactic_variable.rego │ │ └── metasyntactic_variable_test.rego │ │ ├── print-or-trace-call │ │ ├── print_or_trace_call.rego │ │ └── print_or_trace_call_test.rego │ │ ├── test-outside-test-package │ │ ├── test_outside_test_package.rego │ │ └── test_outside_test_package_test.rego │ │ └── todo-test │ │ ├── todo_test.rego │ │ └── todo_test_test.rego │ └── util │ ├── util.rego │ └── util_test.rego ├── cmd ├── capabilities.go ├── constants.go ├── debugger.go ├── exit.go ├── fix.go ├── languageserver.go ├── lint.go ├── new.go ├── parse.go ├── profiling.go ├── root.go ├── test.go ├── utils.go └── version.go ├── docs ├── .markdownlint.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── SECURITY.md ├── adopters.md ├── adopters.md.yaml ├── architecture.md ├── architecture.md.yaml ├── assets │ ├── dap │ │ ├── animation.gif │ │ ├── breakpoint.png │ │ ├── codeaction.png │ │ ├── print.png │ │ └── variables.png │ ├── editors-neovim.png │ ├── evalcustom.png │ ├── lsp │ │ ├── code_action_fix.png │ │ ├── code_action_show.png │ │ ├── codeaction.png │ │ ├── completion.png │ │ ├── diagnostics.png │ │ ├── documentsymbols.png │ │ ├── documentsymbols2.png │ │ ├── eval_use_as_input.png │ │ ├── evalcodelens.png │ │ ├── evalcodelensprint.png │ │ ├── folding.png │ │ ├── format.png │ │ ├── hover.png │ │ └── inlay.png │ ├── regal-banner.png │ ├── regal.jpg │ ├── regal_cncf_london.png │ └── rules │ │ └── pkg_name_completion.png ├── cicd.md ├── cicd.md.yaml ├── custom-rules.md ├── custom-rules.md.yaml ├── debug-adapter.md ├── debug-adapter.md.yaml ├── editor-support.md ├── editor-support.md.yaml ├── fixing.md ├── fixing.md.yaml ├── integration.md ├── integration.md.yaml ├── language-server.md ├── language-server.md.yaml ├── opa-one-dot-zero.md ├── opa-one-dot-zero.md.yaml ├── pre-commit-hooks.md ├── pre-commit-hooks.md.yaml ├── readme-sections │ ├── badges.md │ ├── capabilities.md │ ├── configuration.md │ ├── exit.md │ ├── footer.md │ ├── getting-started.md │ ├── github-manifest │ ├── goals.md │ ├── ignore-rules.md │ ├── image-definition.md │ ├── intro.md │ ├── ls.md │ ├── opa1.md │ ├── output.md │ ├── project-roots.md │ ├── rego-version.md │ ├── resources-website.md │ ├── resources.md │ ├── roadmap-website.md │ ├── roadmap.md │ ├── rules.md │ ├── status.md │ ├── strict.md │ ├── testimonials.md │ ├── title.md │ ├── website-intro.md │ └── website-manifest ├── rego-style-guide.md ├── remote-features.md ├── remote-features.md.yaml └── rules │ ├── _category_.json │ ├── bugs │ ├── annotation-without-metadata.md │ ├── argument-always-wildcard.md │ ├── constant-condition.md │ ├── deprecated-builtin.md │ ├── duplicate-rule.md │ ├── if-empty-object.md │ ├── if-object-literal.md │ ├── import-shadows-rule.md │ ├── impossible-not.md │ ├── inconsistent-args.md │ ├── index.md.json │ ├── internal-entrypoint.md │ ├── invalid-metadata-attribute.md │ ├── leaked-internal-reference.md │ ├── not-equals-in-loop.md │ ├── redundant-existence-check.md │ ├── redundant-loop-count.md │ ├── rule-assigns-default.md │ ├── rule-named-if.md │ ├── rule-shadows-builtin.md │ ├── sprintf-arguments-mismatch.md │ ├── time-now-ns-twice.md │ ├── top-level-iteration.md │ ├── unassigned-return-value.md │ ├── unused-output-variable.md │ ├── unused-return-value.md │ ├── var-shadows-builtin.md │ └── zero-arity-function.md │ ├── custom │ ├── forbidden-function-call.md │ ├── index.md.json │ ├── missing-metadata.md │ ├── naming-convention.md │ ├── narrow-argument.md │ ├── one-liner-rule.md │ └── prefer-value-in-head.md │ ├── idiomatic │ ├── ambiguous-scope.md │ ├── boolean-assignment.md │ ├── custom-has-key-construct.md │ ├── custom-in-construct.md │ ├── directory-package-mismatch.md │ ├── equals-pattern-matching.md │ ├── in-wildcard-key.md │ ├── index.md.json │ ├── no-defined-entrypoint.md │ ├── non-raw-regex-pattern.md │ ├── prefer-set-or-object-rule.md │ ├── single-item-in.md │ ├── use-contains.md │ ├── use-if.md │ ├── use-in-operator.md │ ├── use-object-keys.md │ ├── use-some-for-output-vars.md │ └── use-strings-count.md │ ├── imports │ ├── avoid-importing-input.md │ ├── circular-import.md │ ├── confusing-alias.md │ ├── ignored-import.md │ ├── implicit-future-keywords.md │ ├── import-after-rule.md │ ├── import-shadows-builtin.md │ ├── import-shadows-import.md │ ├── index.md.json │ ├── pointless-import.md │ ├── prefer-package-imports.md │ ├── redundant-alias.md │ ├── redundant-data-import.md │ ├── unresolved-import.md │ ├── unresolved-reference.md │ └── use-rego-v1.md │ ├── performance │ ├── defer-assignment.md │ ├── index.md.json │ ├── non-loop-expression.md │ ├── walk-no-path.md │ └── with-outside-test-context.md │ ├── style │ ├── avoid-get-and-list-prefix.md │ ├── chained-rule-body.md │ ├── comprehension-term-assignment.md │ ├── default-over-else.md │ ├── default-over-not.md │ ├── detached-metadata.md │ ├── double-negative.md │ ├── external-reference.md │ ├── file-length.md │ ├── function-arg-return.md │ ├── index.md.json │ ├── line-length.md │ ├── messy-rule.md │ ├── mixed-iteration.md │ ├── no-whitespace-comment.md │ ├── opa-fmt.md │ ├── pointless-reassignment.md │ ├── prefer-snake-case.md │ ├── prefer-some-in-iteration.md │ ├── rule-length.md │ ├── rule-name-repeats-package.md │ ├── todo-comment.md │ ├── trailing-default-rule.md │ ├── unconditional-assignment.md │ ├── unnecessary-some.md │ ├── use-assignment-operator.md │ ├── use-in-operator.md │ └── yoda-condition.md │ └── testing │ ├── dubious-print-sprintf.md │ ├── file-missing-test-suffix.md │ ├── identically-named-tests.md │ ├── index.md.json │ ├── metasyntactic-variable.md │ ├── print-or-trace-call.md │ ├── test-outside-test-package.md │ └── todo-test.md ├── e2e ├── cli_test.go ├── e2e_conf.yaml └── testdata │ ├── aggregates │ ├── custom │ │ └── regal │ │ │ └── rules │ │ │ └── testcase │ │ │ ├── aggregates │ │ │ └── custom_rules_using_aggregates.rego │ │ │ └── empty_aggregate │ │ │ └── empty_aggregate.rego │ ├── ignore_directive │ │ ├── first.rego │ │ └── second.rego │ ├── three_policies │ │ ├── policy_1.rego │ │ ├── policy_2.rego │ │ └── policy_3.rego │ └── two_policies │ │ ├── policy_1.rego │ │ └── policy_2.rego │ ├── ast_type_failure │ └── custom_type_fail.rego │ ├── bugs │ └── issue_1082.rego │ ├── capabilities │ ├── custom_has_key.rego │ └── custom_has_key_2.rego │ ├── configs │ ├── custom_naming_convention.yaml │ ├── defaulting.yaml │ ├── ignore_files_prefer_snake_case.yaml │ ├── opa_v46_capabilities.yaml │ ├── rule_without_level.yaml │ ├── v0-with-import-rego-v1.yaml │ └── v0.yaml │ ├── custom_naming_convention │ └── policy.rego │ ├── custom_rules │ └── custom.rego │ ├── defaulting │ └── example.rego │ ├── v0 │ ├── not_rego_v1.rego │ └── rule_named_if.rego │ └── violations │ ├── circular_import.rego │ └── most_violations.rego ├── go.mod ├── go.sum ├── internal ├── ast │ ├── rule.go │ └── rule_test.go ├── cache │ ├── cache.go │ └── cache_test.go ├── capabilities │ ├── capabilities.go │ ├── capabilities_integration_test.go │ ├── capabilities_test.go │ ├── embedded │ │ ├── embedded.go │ │ ├── embedded_test.go │ │ └── eopa │ │ │ ├── v1.0.0.json │ │ │ ├── v1.0.1.json │ │ │ ├── v1.1.0.json │ │ │ ├── v1.10.0.json │ │ │ ├── v1.10.1.json │ │ │ ├── v1.11.0.json │ │ │ ├── v1.11.1.json │ │ │ ├── v1.12.0.json │ │ │ ├── v1.13.0.json │ │ │ ├── v1.14.0.json │ │ │ ├── v1.15.1.json │ │ │ ├── v1.15.2.json │ │ │ ├── v1.15.3.json │ │ │ ├── v1.15.4.json │ │ │ ├── v1.15.5.json │ │ │ ├── v1.16.0.json │ │ │ ├── v1.17.0.json │ │ │ ├── v1.17.1.json │ │ │ ├── v1.17.2.json │ │ │ ├── v1.18.0.json │ │ │ ├── v1.18.1.json │ │ │ ├── v1.19.0.json │ │ │ ├── v1.2.0.json │ │ │ ├── v1.20.0.json │ │ │ ├── v1.21.0.json │ │ │ ├── v1.22.0.json │ │ │ ├── v1.23.0.json │ │ │ ├── v1.24.0.json │ │ │ ├── v1.24.1.json │ │ │ ├── v1.24.2.json │ │ │ ├── v1.24.3.json │ │ │ ├── v1.24.4.json │ │ │ ├── v1.24.5.json │ │ │ ├── v1.24.6.json │ │ │ ├── v1.24.7.json │ │ │ ├── v1.24.8.json │ │ │ ├── v1.25.0.json │ │ │ ├── v1.25.1.json │ │ │ ├── v1.26.0.json │ │ │ ├── v1.27.0.json │ │ │ ├── v1.27.1.json │ │ │ ├── v1.28.0.json │ │ │ ├── v1.29.0.json │ │ │ ├── v1.29.1.json │ │ │ ├── v1.3.0.json │ │ │ ├── v1.30.0.json │ │ │ ├── v1.30.1.json │ │ │ ├── v1.31.0.json │ │ │ ├── v1.31.1.json │ │ │ ├── v1.31.2.json │ │ │ ├── v1.31.3.json │ │ │ ├── v1.32.0.json │ │ │ ├── v1.32.1.json │ │ │ ├── v1.33.0.json │ │ │ ├── v1.34.0.json │ │ │ ├── v1.35.0.json │ │ │ ├── v1.36.0.json │ │ │ ├── v1.36.1.json │ │ │ ├── v1.36.2.json │ │ │ ├── v1.37.0.json │ │ │ ├── v1.38.0.json │ │ │ ├── v1.39.0.json │ │ │ ├── v1.39.1.json │ │ │ ├── v1.4.0.json │ │ │ ├── v1.40.0.json │ │ │ ├── v1.41.0.json │ │ │ ├── v1.5.1.json │ │ │ ├── v1.6.0.json │ │ │ ├── v1.7.0.json │ │ │ ├── v1.8.0.json │ │ │ ├── v1.9.0.json │ │ │ ├── v1.9.2.json │ │ │ ├── v1.9.3.json │ │ │ ├── v1.9.4.json │ │ │ └── v1.9.5.json │ └── testdata │ │ └── capabilities.json ├── compile │ └── compile.go ├── dap │ ├── dap.go │ └── logger.go ├── docs │ └── docs.go ├── embeds │ ├── embeds.go │ ├── schemas │ │ ├── aggregate.json │ │ └── regal-ast.json │ └── templates │ │ ├── builtin │ │ ├── builtin.md.tpl │ │ ├── builtin.rego.tpl │ │ └── builtin_test.rego.tpl │ │ └── custom │ │ ├── custom.rego.tpl │ │ └── custom_test.rego.tpl ├── explorer │ └── stages.go ├── git │ └── git.go ├── io │ ├── io.go │ └── io_test.go ├── lsp │ ├── bundles │ │ ├── bundles.go │ │ ├── bundles_test.go │ │ ├── cache.go │ │ └── cache_test.go │ ├── cache │ │ ├── cache.go │ │ └── cache_test.go │ ├── clients │ │ └── clients.go │ ├── command.go │ ├── command_test.go │ ├── commands │ │ ├── parse.go │ │ └── parse_test.go │ ├── completions │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── providers │ │ │ ├── builtins.go │ │ │ ├── builtins_test.go │ │ │ ├── options.go │ │ │ ├── packagerefs.go │ │ │ ├── packagerefs_test.go │ │ │ ├── policy.go │ │ │ ├── policy_test.go │ │ │ ├── rulehead.go │ │ │ ├── rulehead_test.go │ │ │ ├── ruleheadkeyword.go │ │ │ ├── ruleheadkeyword_test.go │ │ │ ├── shared_test.go │ │ │ └── utils.go │ │ └── refs │ │ │ ├── defined.go │ │ │ ├── defined_test.go │ │ │ ├── used.go │ │ │ └── used_test.go │ ├── config │ │ ├── find.go │ │ ├── find_test.go │ │ ├── watcher.go │ │ └── watcher_test.go │ ├── connection.go │ ├── diff.go │ ├── documentsymbol.go │ ├── documentsymbol_test.go │ ├── eval.go │ ├── eval_test.go │ ├── examples │ │ ├── examples.go │ │ └── index.json │ ├── foldingrange.go │ ├── foldingrange_test.go │ ├── format.go │ ├── handle_text_document_code_action_test.go │ ├── handler │ │ └── handler.go │ ├── hover │ │ ├── hover.go │ │ ├── hover_test.go │ │ └── testdata │ │ │ └── hover │ │ │ ├── foobar.md │ │ │ ├── graphreachable.md │ │ │ ├── indexof.md │ │ │ └── jsonfilter.md │ ├── inlayhint.go │ ├── inlayhint_test.go │ ├── lint.go │ ├── lint_test.go │ ├── log │ │ ├── level.go │ │ └── level_test.go │ ├── opa │ │ ├── scanner │ │ │ └── scanner.go │ │ └── tokens │ │ │ └── tokens.go │ ├── race_off.go │ ├── race_on.go │ ├── rego │ │ ├── builtins.go │ │ ├── rego.go │ │ └── rego_test.go │ ├── server.go │ ├── server_aggregates_test.go │ ├── server_builtins_test.go │ ├── server_config_test.go │ ├── server_formatting_test.go │ ├── server_multi_file_test.go │ ├── server_rename_test.go │ ├── server_single_file_test.go │ ├── server_template_test.go │ ├── server_test.go │ ├── stdio.go │ ├── store.go │ ├── store_test.go │ ├── types │ │ ├── completion │ │ │ └── completion.go │ │ ├── internal.go │ │ ├── symbols │ │ │ └── symbols.go │ │ └── types.go │ └── uri │ │ ├── uri.go │ │ └── uri_test.go ├── metrics │ └── metrics.go ├── mode │ ├── standalone.go │ └── standalone_flag.go ├── novelty │ ├── holidays.go │ └── novelty.go ├── parse │ ├── parse.go │ └── parse_test.go ├── test │ └── test.go ├── testutil │ └── testutil.go ├── update │ ├── update.go │ ├── update.rego │ └── update_test.go ├── util │ ├── ast.go │ ├── util.go │ └── util_test.go └── web │ ├── assets │ ├── htmx-1.8.4.min.js │ ├── main.tpl │ └── missing.min.css │ ├── server.go │ └── server_test.go ├── main.go └── pkg ├── builtins ├── builtins.go └── builtins_test.go ├── config ├── bundle.go ├── config.go ├── config_test.go ├── filter.go ├── filter_test.go ├── fixtures │ └── caps.json └── global.go ├── fixer ├── fileprovider │ ├── cache.go │ ├── cache_test.go │ ├── fp.go │ ├── inmem.go │ └── inmem_test.go ├── fixer.go ├── fixer_test.go ├── fixes │ ├── directorypackagemismatch.go │ ├── directorypackagemismatch_test.go │ ├── fixes.go │ ├── fmt.go │ ├── fmt_test.go │ ├── nonrawregexpattern.go │ ├── nonrawregexpattern_test.go │ ├── nowhitespacecomment.go │ ├── nowhitespacecomment_test.go │ ├── useassignmentoperator.go │ └── useassignmentoperator_test.go ├── rename.go ├── rename_test.go ├── report.go ├── reporter.go └── reporter_test.go ├── hints ├── hints.go └── hints_test.go ├── linter ├── linter.go ├── linter_test.go └── testdata │ ├── custom.rego │ └── printer.rego ├── report └── report.go ├── reporter ├── reporter.go ├── reporter_test.go └── testdata │ ├── json │ ├── reporter-no-violations.json │ └── reporter.json │ ├── junit │ └── reporter.xml │ ├── pretty │ ├── reporter-long-text.txt │ └── reporter.txt │ └── sarif │ ├── reporter-no-region.json │ ├── reporter-no-violation.json │ └── reporter.json ├── rules ├── rules.go └── rules_test.go └── version └── version.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{rego,go}] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.jpg binary 4 | *.png binary 5 | *.gif binary 6 | 7 | *.rego.tpl linguist-language=Open-Policy-Agent 8 | *.rq linguist-language=Open-Policy-Agent 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: "gomod" 12 | directory: "/" 13 | schedule: 14 | interval: "monthly" 15 | ignore: 16 | # update OPA manually to bump version in README too 17 | - dependency-name: "github.com/open-policy-agent/opa/v1" 18 | groups: 19 | dependencies: 20 | patterns: 21 | - "*" 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | schedule: 5 | - cron: "19 18 * * 4" 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 14 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: ["go"] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 39 | with: 40 | category: "/language:${{matrix.language}}" 41 | -------------------------------------------------------------------------------- /.github/workflows/update-docs.yaml: -------------------------------------------------------------------------------- 1 | # this workflow is used to update the Regal content at docs.styra.com 2 | # when it changes in this repo. 3 | name: Update Docs 4 | 5 | on: 6 | push: 7 | tags: 8 | - v[0-9].** 9 | workflow_dispatch: 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | update-docs: 15 | name: Update Docs 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Save version 24 | run: | 25 | mkdir -p versions 26 | echo "${{ github.sha }}" > versions/regal 27 | 28 | - name: Update docs 29 | uses: leigholiver/commit-with-deploy-key@64d2c8705aa10aa475e971b877a7fe6ada69a1a2 30 | with: 31 | source: versions 32 | destination_folder: imported/versions 33 | destination_repo: StyraInc/docs 34 | deploy_key: ${{ secrets.STYRA_DOCS_DEPLOY_KEY }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | dist/ 4 | 5 | /regal 6 | /regal.exe 7 | 8 | # These two files are used by the Regal evaluation Code Lens, where input.json 9 | # (or input.yaml) defines the input to use for evaluation, and output.json is 10 | # where the output ends up unless the client supports presenting it in a 11 | # different way. 12 | input.json 13 | input.yaml 14 | 15 | output.json 16 | 17 | build/node_modules/ 18 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: regal-lint 2 | language: golang 3 | name: policy file linting 4 | description: policy file linting with Regal from Styra built from source 5 | entry: regal lint 6 | files: (\.rego)$ 7 | 8 | - id: regal-lint-use-path 9 | language: system 10 | name: policy file linting 11 | description: policy file linting with Regal from Styra using system $PATH 12 | entry: regal lint 13 | files: (\.rego)$ 14 | 15 | - id: regal-download 16 | language: script 17 | name: policy file linting 18 | description: policy file linting with Regal downloaded from Github 19 | entry: build/download-and-run.sh lint 20 | files: (\.rego)$ 21 | -------------------------------------------------------------------------------- /.regal/config.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | files: 3 | - e2e/* 4 | - pkg/* 5 | 6 | rules: 7 | custom: 8 | missing-metadata: 9 | level: error 10 | except-rule-path-pattern: \.report$ 11 | # TODO: this should be in the default config, but it seems 12 | # like the ignore attribute isn't read from there 13 | ignore: 14 | files: 15 | - "*_test.rego" 16 | narrow-argument: 17 | level: error 18 | exclude-args: 19 | - cfg 20 | - metadata 21 | - rule 22 | one-liner-rule: 23 | level: error 24 | prefer-value-in-head: 25 | level: error 26 | only-scalars: true 27 | style: 28 | line-length: 29 | level: error 30 | non-breakable-word-threshold: 100 31 | imports: 32 | unresolved-reference: 33 | level: error 34 | except-paths: 35 | - data.eval.** 36 | - data.internal** 37 | - data.workspace.** 38 | - data.regal.config.provided** 39 | - data.custom.regal** 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "regal lint bundle", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "${workspaceFolder}", 10 | "args": [ 11 | "lint", 12 | "--enable-print", 13 | "bundle" 14 | ] 15 | }, 16 | { 17 | "name": "regal fix --dry-run bundle", 18 | "type": "go", 19 | "request": "launch", 20 | "mode": "auto", 21 | "program": "${workspaceFolder}", 22 | "args": [ 23 | "fix", 24 | "--dry-run", 25 | "bundle" 26 | ] 27 | }, 28 | { 29 | "name": "regal test bundle", 30 | "type": "go", 31 | "request": "launch", 32 | "mode": "auto", 33 | "program": "${workspaceFolder}", 34 | "args": [ 35 | "test", 36 | "bundle" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "opa.env": { 3 | "OPA_CHECK_CAPABILITIES": "${workspacePath}/build/capabilities.json", 4 | "OPA_EVAL_CAPABILITIES": "${workspacePath}/build/capabilities.json" 5 | }, 6 | "opa.roots": [ 7 | "${workspaceFolder}/bundle" 8 | ], 9 | "opa.strictMode": true, 10 | "go.lintTool": "golangci-lint", 11 | "go.buildFlags": [ 12 | "-tags", 13 | "e2e" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "regal: prepare pr", 6 | "type": "shell", 7 | "command": "./build/do.rq pr", 8 | "detail": "Prepare PR", 9 | "options": { 10 | "cwd": "${workspaceFolder}" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "label": "regal: regal test", 19 | "type": "shell", 20 | "command": "go run main.go test bundle", 21 | "group": "test", 22 | "options": { 23 | "cwd": "${workspaceFolder}" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /build/download-and-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # pre-commit helper to run download latest release binary if missing before executing 4 | # linting with it. 5 | 6 | set -e 7 | 8 | REPO=StyraInc/regal 9 | BASE_URL=https://github.com/${REPO} 10 | 11 | SCRIPT=$(readlink -f "$0") 12 | SCRIPTPATH=$(dirname "$SCRIPT") 13 | BIN_PATH="${SCRIPTPATH}/regal" 14 | 15 | download() 16 | { 17 | DETECTED_SYSTEM=$(uname -s) 18 | DETECTED_ARCHITECTURE=$(uname -m) 19 | 20 | REGAL_VERSION=${REGAL_VERSION:-latest} 21 | SYSTEM=${REGAL_SYSTEM:-${DETECTED_SYSTEM}} 22 | ARCHITECTURE=${REGAL_ARCHITECTURE:-${DETECTED_ARCHITECTURE}} 23 | 24 | echo "Downloading regal for ${SYSTEM} ${ARCHITECTURE}, ${REGAL_VERSION}…" 25 | BINARY_URL=${BASE_URL}/releases/${REGAL_VERSION}/download/regal_${SYSTEM}_${ARCHITECTURE} 26 | curl --fail -Lo "${BIN_PATH}" ${BINARY_URL} 27 | chmod +x "${BIN_PATH}" 28 | } 29 | 30 | if [ ! -x "${BIN_PATH}" ]; then download; fi 31 | "${BIN_PATH}" $@ 32 | -------------------------------------------------------------------------------- /build/dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml": { 3 | "indentBlockSequenceInMap": true 4 | }, 5 | "json": {}, 6 | "excludes": [ 7 | ".lsp/**", 8 | "**/testdata/**" 9 | ], 10 | "plugins": [ 11 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm", 12 | "https://plugins.dprint.dev/json-0.19.3.wasm" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regal-build-tools", 3 | "version": "0.1.0", 4 | "description": "JS based tooling used in the Regal project", 5 | "license": "Apache-2.0", 6 | "dependencies": { 7 | "dprint": "^0.47.2", 8 | "markdownlint-cli": "^0.42.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /build/update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | README_SECTIONS_DIR="./docs/readme-sections" 4 | README_PATH="./README.md" 5 | MANIFEST="$README_SECTIONS_DIR/github-manifest" 6 | 7 | if [[ $# -ne 1 ]]; then 8 | echo "Usage: $0 {check|write}" 9 | exit 1 10 | fi 11 | 12 | MODE="$1" 13 | 14 | # Create a temporary file to hold the new content 15 | tmpfile=$(mktemp) 16 | 17 | # Build new content into tmpfile 18 | while IFS= read -r file; do 19 | section_path="$README_SECTIONS_DIR/$file" 20 | 21 | if [[ -f "$section_path" ]]; then 22 | cat "$section_path" >> "$tmpfile" 23 | echo -e "\n" >> "$tmpfile" 24 | else 25 | echo "Warning: Section file not found: $section_path" >&2 26 | fi 27 | done < "$MANIFEST" 28 | 29 | if [[ "$MODE" == "check" ]]; then 30 | if ! cmp -s "$tmpfile" "$README_PATH"; then 31 | echo "README.md is out of date. Please run '$0 write' to update it." 32 | rm "$tmpfile" 33 | exit 1 34 | else 35 | echo "README.md is up to date." 36 | fi 37 | elif [[ "$MODE" == "write" ]]; then 38 | mv "$tmpfile" "$README_PATH" 39 | echo "README.md has been updated." 40 | else 41 | echo "Unknown mode: $MODE" 42 | echo "Usage: $0 {check|write}" 43 | rm "$tmpfile" 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /bundle/.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "roots": ["regal"], 3 | "metadata": { 4 | "name": "regal" 5 | }, 6 | "rego_version": 1 7 | } 8 | -------------------------------------------------------------------------------- /bundle/bundle.go: -------------------------------------------------------------------------------- 1 | package bundle 2 | 3 | import ( 4 | "embed" 5 | 6 | rio "github.com/styrainc/regal/internal/io" 7 | ) 8 | 9 | // Bundle FS will include the tests as well, but since that has negligible impact on the size of the binary, 10 | // it's preferable to filter them out from the bundle than to e.g. create a separate directory for tests 11 | // 12 | //go:embed * 13 | var Bundle embed.FS 14 | 15 | // LoadedBundle contains the loaded contents of the Bundle. 16 | var LoadedBundle = rio.MustLoadRegalBundleFS(Bundle) 17 | -------------------------------------------------------------------------------- /bundle/regal/ast/testing.rego: -------------------------------------------------------------------------------- 1 | package regal.ast 2 | 3 | # METADATA 4 | # description: | 5 | # parses provided policy with all future keywords imported. Primarily for testing. 6 | # deprecated: use ast.policy instead 7 | with_rego_v1(policy) := regal.parse_module("policy.rego", concat("", [ 8 | `package policy 9 | 10 | import rego.v1 11 | 12 | `, 13 | policy, 14 | ])) 15 | 16 | # METADATA 17 | # description: parses provided policy with v0 syntax and no imports. Primarily for testing. 18 | with_rego_v0(policy) := regal.parse_module("policy_v0.rego", concat("", [ 19 | `package policy 20 | 21 | `, 22 | policy, 23 | ])) 24 | 25 | # METADATA 26 | # description: parse provided snippet with a generic package declaration added 27 | policy(snippet) := regal.parse_module("policy.rego", concat("", [ 28 | "package policy\n\n", 29 | snippet, 30 | ])) 31 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/kind/kind_test.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion.kind_test 2 | 3 | import data.regal.lsp.completion.kind 4 | 5 | test_kind_for_coverage if { 6 | kind_values := [value | some value in kind] 7 | sort(kind_values) == numbers.range(1, 25) 8 | } 9 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/main.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: | 3 | # base package for completion suggestion provider policies, and acts 4 | # like a router that collects suggestions from all provider policies 5 | # under regal.lsp.completion.providers 6 | package regal.lsp.completion 7 | 8 | # METADATA 9 | # description: main entry point for completion suggestions 10 | # entrypoint: true 11 | items contains object.union(completion, {"_regal": {"provider": provider}}) if { 12 | some provider, completion 13 | data.regal.lsp.completion.providers[provider].items[completion] 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/main_test.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion_test 2 | 3 | import data.regal.lsp.completion 4 | 5 | test_completion_entrypoint if { 6 | items := completion.items with completion.providers as {"test": {"items": {{"foo": "bar"}}}} 7 | 8 | items == {{"_regal": {"provider": "test"}, "foo": "bar"}} 9 | } 10 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/booleans/booleans.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: the boolean provider suggests `true`/`false` values where appropriate 3 | package regal.lsp.completion.providers.booleans 4 | 5 | import data.regal.lsp.completion.kind 6 | import data.regal.lsp.completion.location 7 | 8 | # METADATA 9 | # description: completion suggestions for true/false 10 | items contains item if { 11 | position := location.to_position(input.regal.context.location) 12 | 13 | line := input.regal.file.lines[position.line] 14 | line != "" 15 | 16 | words := regex.split(`\s+`, line) 17 | 18 | words_on_line := count(words) 19 | previous_word := words[words_on_line - 2] 20 | 21 | previous_word in {"==", ":="} 22 | 23 | word := location.word_at(line, input.regal.context.location.col) 24 | 25 | some b in ["true", "false"] 26 | 27 | startswith(b, word.text) 28 | 29 | item := { 30 | "label": b, 31 | "kind": kind.constant, 32 | "detail": "boolean value", 33 | "textEdit": { 34 | "range": location.word_range(word, position), 35 | "newText": b, 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/commonrule/commonrule.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: | 3 | # provides completions for common rule names, like 'allow' or 'deny' 4 | package regal.lsp.completion.providers.commonrule 5 | 6 | import data.regal.lsp.completion.kind 7 | import data.regal.lsp.completion.location 8 | 9 | _suggested_names := { 10 | "allow", 11 | "authorized", 12 | "deny", 13 | } 14 | 15 | # METADATA 16 | # description: all completion suggestions for common rule names 17 | items contains item if { 18 | position := location.to_position(input.regal.context.location) 19 | line := input.regal.file.lines[position.line] 20 | 21 | some label in _suggested_names 22 | 23 | startswith(label, line) 24 | 25 | item := { 26 | "label": label, 27 | "kind": kind.snippet, 28 | "detail": "common name", 29 | "documentation": { 30 | "kind": "markdown", 31 | "value": sprintf("%q is a common rule name", [label]), 32 | }, 33 | "textEdit": { 34 | "range": location.from_start_of_line_to_position(position), 35 | "newText": concat("", [label, " "]), 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/import/import.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: provides completion suggestions for the `import` keyword where applicable 3 | package regal.lsp.completion.providers["import"] 4 | 5 | import data.regal.lsp.completion.kind 6 | import data.regal.lsp.completion.location 7 | 8 | # METADATA 9 | # description: all completion suggestions for the import keyword 10 | items contains item if { 11 | position := location.to_position(input.regal.context.location) 12 | line := input.regal.file.lines[position.line] 13 | 14 | startswith("import", line) 15 | 16 | word := location.word_at(line, input.regal.context.location.col) 17 | 18 | item := { 19 | "label": "import", 20 | "kind": kind.keyword, 21 | "detail": "import ", 22 | "textEdit": { 23 | "range": location.word_range(word, position), 24 | "newText": "import ", 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/import/import_test.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion.providers.import_test 2 | 3 | import data.regal.lsp.completion.providers["import"] as provider 4 | import data.regal.lsp.completion.providers.test_utils as util 5 | 6 | test_import_completion_empty_line if { 7 | policy := `package policy 8 | 9 | import rego.v1 10 | 11 | ` 12 | items := provider.items with input as util.input_with_location(policy, {"row": 5, "col": 1}) 13 | items == {{ 14 | "label": "import", 15 | "detail": "import ", 16 | "kind": 14, 17 | "textEdit": { 18 | "newText": "import ", 19 | "range": { 20 | "start": {"character": 0, "line": 4}, 21 | "end": {"character": 0, "line": 4}, 22 | }, 23 | }, 24 | }} 25 | } 26 | 27 | test_import_completion_on_typing if { 28 | policy := `package policy 29 | 30 | import rego.v1 31 | 32 | imp` 33 | 34 | items := provider.items with input as util.input_with_location(policy, {"row": 5, "col": 3}) 35 | 36 | items == {{ 37 | "label": "import", 38 | "detail": "import ", 39 | "kind": 14, 40 | "textEdit": { 41 | "newText": "import ", 42 | "range": { 43 | "start": {"character": 0, "line": 4}, 44 | "end": {"character": 3, "line": 4}, 45 | }, 46 | }, 47 | }} 48 | } 49 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/input/input_test.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion.providers.input_test 2 | 3 | import data.regal.lsp.completion.providers.input as provider 4 | import data.regal.lsp.completion.providers.test_utils as util 5 | 6 | test_input_completion_on_typing if { 7 | policy := `package policy 8 | 9 | allow if { 10 | i 11 | }` 12 | items := provider.items with input as util.input_with_location(policy, {"row": 4, "col": 3}) 13 | 14 | items == {{ 15 | "detail": "input document", 16 | "documentation": { 17 | "kind": "markdown", 18 | "value": provider._doc, 19 | }, 20 | "kind": 14, 21 | "label": "input", 22 | "textEdit": { 23 | "newText": "input", 24 | "range": { 25 | "end": { 26 | "character": 2, 27 | "line": 3, 28 | }, 29 | "start": { 30 | "character": 1, 31 | "line": 3, 32 | }, 33 | }, 34 | }, 35 | }} 36 | } 37 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/package/package.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: provides completion suggestions for the `package` keyword where applicable 3 | package regal.lsp.completion.providers["package"] 4 | 5 | import data.regal.lsp.completion.kind 6 | import data.regal.lsp.completion.location 7 | 8 | # METADATA 9 | # description: completion suggestions for package keyword 10 | items contains item if { 11 | not strings.any_prefix_match(input.regal.file.lines, "package ") 12 | 13 | position := location.to_position(input.regal.context.location) 14 | line := input.regal.file.lines[position.line] 15 | 16 | startswith("package", line) 17 | 18 | item := { 19 | "label": "package", 20 | "kind": kind.keyword, 21 | "detail": "package ", 22 | "textEdit": { 23 | "range": location.from_start_of_line_to_position(position), 24 | "newText": "package ", 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/providers/regov1/regov1.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: | 3 | # the `regov1`` provider provides completion suggestions for 4 | # `rego.v1` following an `import` declaration 5 | package regal.lsp.completion.providers.regov1 6 | 7 | import data.regal.lsp.completion.kind 8 | import data.regal.lsp.completion.location 9 | 10 | # METADATA 11 | # description: completion suggestion for rego.v1 12 | items contains item if { 13 | input.regal.context.rego_version != 3 # the rego.v1 import is not used in v1 rego (3) 14 | not strings.any_prefix_match(input.regal.file.lines, "import rego.v1") 15 | 16 | position := location.to_position(input.regal.context.location) 17 | line := input.regal.file.lines[position.line] 18 | 19 | startswith(line, "import ") 20 | 21 | word := location.ref_at(line, input.regal.context.location.col) 22 | 23 | startswith("rego.v1", word.text) 24 | 25 | item := { 26 | "label": "rego.v1", 27 | "kind": kind.module, 28 | "detail": "use rego.v1", 29 | "textEdit": { 30 | "range": location.word_range(word, position), 31 | "newText": "rego.v1\n\n", 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/ref_names.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion 2 | 3 | import data.regal.ast 4 | 5 | # METADATA 6 | # description: | 7 | # returns a list of ref names that are used in the module 8 | # built-in functions are not included as they are provided by another completions provider 9 | # scope: document 10 | ref_names contains name if { 11 | name := ast.ref_static_to_string(ast.found.calls[_][_].value) 12 | 13 | not name in ast.builtin_names 14 | } 15 | 16 | ref_names contains name if { 17 | name := ast.ref_static_to_string(ast.found.refs[_][_].value) 18 | 19 | not name in ast.builtin_names 20 | } 21 | 22 | # if a user has imported data.foo, then foo should be suggested. 23 | # if they have imported data.foo as bar, then bar should be suggested. 24 | # this also has the benefit of skipping future.* and rego.v1 as 25 | # imported_identifiers will only match data.* and input.* 26 | ref_names contains name if some name in ast.imported_identifiers 27 | -------------------------------------------------------------------------------- /bundle/regal/lsp/completion/ref_names_test.rego: -------------------------------------------------------------------------------- 1 | package regal.lsp.completion_test 2 | 3 | import data.regal.ast 4 | import data.regal.capabilities 5 | import data.regal.config 6 | 7 | import data.regal.lsp.completion 8 | 9 | test_ref_names if { 10 | module := ast.policy(` 11 | import data.imp 12 | import data.foo.bar as bb 13 | 14 | x := 1 15 | 16 | allow if { 17 | some x 18 | input.foo[x] == data.bar[x] 19 | startswith("hey", "h") 20 | 21 | imp.foo == data.x 22 | } 23 | `) 24 | ref_names := completion.ref_names with input as module with config.capabilities as capabilities.provided 25 | 26 | ref_names == { 27 | "imp", 28 | "bb", 29 | "input.foo", 30 | "data.bar", 31 | "imp.foo", 32 | "data.x", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bundle/regal/lsp/util/location/location.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: utility functions for dealing with location data in the LSP 3 | package regal.lsp.util.location 4 | 5 | # METADATA 6 | # description: turns an AST location (with `end`` attribute) into an LSP range 7 | to_range(location) := { 8 | "start": { 9 | "line": location.row - 1, 10 | "character": location.col - 1, 11 | }, 12 | "end": { 13 | "line": location.end.row - 1, 14 | "character": location.end.col - 1, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /bundle/regal/regal.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # scope: subpackages 3 | # authors: 4 | # - Styra 5 | # related_resources: 6 | # - https://www.styra.com 7 | # schemas: 8 | # - input: schema.regal.ast 9 | package regal 10 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/annotation-without-metadata/annotation_without_metadata.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Annotation without metadata 3 | package regal.rules.bugs["annotation-without-metadata"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | some block in ast.comments.blocks 11 | 12 | location := util.to_location_object(block[0].location) 13 | 14 | location.col == 1 15 | ast.comments.annotation_match(block[0].text) 16 | 17 | violation := result.fail(rego.metadata.chain(), result.location(block[0])) 18 | } 19 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/argument-always-wildcard/argument_always_wildcard.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Argument is always a wildcard 3 | package regal.rules.bugs["argument-always-wildcard"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | import data.regal.util 9 | 10 | report contains violation if { 11 | some name, functions in _function_groups 12 | 13 | fn := util.any_set_item(functions) 14 | 15 | some pos in numbers.range(0, count(fn.head.args) - 1) 16 | 17 | every function in functions { 18 | function.head.args[pos].type == "var" 19 | ast.is_wildcard(function.head.args[pos]) 20 | } 21 | 22 | not _function_name_excepted(name) 23 | 24 | violation := result.fail(rego.metadata.chain(), result.location(fn.head.args[pos])) 25 | } 26 | 27 | _function_groups[name] contains fn if { 28 | some fn in ast.functions 29 | 30 | name := ast.ref_to_string(fn.head.ref) 31 | } 32 | 33 | _function_name_excepted(name) if { 34 | regex.match(config.rules.bugs["argument-always-wildcard"]["except-function-name-pattern"], name) 35 | } 36 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/constant-condition/constant_condition.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Constant condition 3 | package regal.rules.bugs["constant-condition"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | # METADATA 9 | # description: single scalar value, like a lone `true` inside a rule body 10 | # scope: rule 11 | report contains violation if { 12 | terms := ast.found.expressions[_][_].terms 13 | 14 | # We could include composite types too, but less comomon and more expensive to check 15 | terms.type in ast.scalar_types 16 | 17 | violation := result.fail(rego.metadata.chain(), result.location(terms)) 18 | } 19 | 20 | # METADATA 21 | # description: two scalar values with a "boolean operator" between, like 1 == 1, or 2 > 1 22 | # scope: rule 23 | report contains violation if { 24 | operators := {"equal", "gt", "gte", "lt", "lte", "neq"} 25 | 26 | expr := ast.found.expressions[_][_] 27 | 28 | expr.terms[0].value[0].type == "var" 29 | expr.terms[0].value[0].value in operators 30 | 31 | expr.terms[1].type in ast.scalar_types 32 | expr.terms[2].type in ast.scalar_types 33 | 34 | violation := result.fail(rego.metadata.chain(), result.location(expr)) 35 | } 36 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/deprecated-builtin/deprecated_builtin.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid using deprecated built-in functions 3 | package regal.rules.bugs["deprecated-builtin"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.config 8 | import data.regal.result 9 | import data.regal.util 10 | 11 | # METADATA 12 | # description: | 13 | # Since OPA 1.0, deprecated-builtin enabled only when provided a v0 policy, 14 | # BUT please note that this may change in the future if new built-in functions 15 | # are deprecated. 16 | # custom: 17 | # severity: none 18 | notices contains result.notice(rego.metadata.chain()) if { 19 | capabilities.is_opa_v1 20 | input.regal.file.rego_version != "v0" 21 | } 22 | 23 | report contains violation if { 24 | deprecated_builtins := { 25 | "any", "all", "re_match", "net.cidr_overlap", "set_diff", "cast_array", 26 | "cast_set", "cast_string", "cast_boolean", "cast_null", "cast_object", 27 | } 28 | 29 | # bail out early if no the deprecated built-ins are in capabilities 30 | util.intersects(object.keys(config.capabilities.builtins), deprecated_builtins) 31 | 32 | call := ast.found.calls[_][_][0] 33 | 34 | ast.ref_to_string(call.value) in deprecated_builtins 35 | 36 | violation := result.fail(rego.metadata.chain(), result.location(call)) 37 | } 38 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/deprecated-builtin/deprecated_builtin_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["deprecated-builtin_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.bugs["deprecated-builtin"] as rule 7 | 8 | test_fail_call_to_deprecated_builtin_function if { 9 | module := ast.with_rego_v1(` 10 | allow if { 11 | any([true, false]) 12 | } 13 | `) 14 | 15 | r := rule.report with input as module with config.capabilities as {"builtins": {"any": {}}} 16 | r == {{ 17 | "category": "bugs", 18 | "description": "Avoid using deprecated built-in functions", 19 | "level": "error", 20 | "location": { 21 | "col": 3, 22 | "file": "policy.rego", 23 | "row": 7, 24 | "text": "\t\tany([true, false])", 25 | "end": {"col": 6, "row": 7}, 26 | }, 27 | "related_resources": [{ 28 | "description": "documentation", 29 | "ref": config.docs.resolve_url("$baseUrl/$category/deprecated-builtin", "bugs"), 30 | }], 31 | "title": "deprecated-builtin", 32 | }} 33 | } 34 | 35 | test_success_deprecated_builtin_not_in_capabilities if { 36 | module := ast.with_rego_v1(` 37 | allow if { 38 | any([true, false]) 39 | } 40 | `) 41 | 42 | r := rule.report with input as module with config.capabilities as {"builtins": {"http.send": {}}} 43 | r == set() 44 | } 45 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/if-empty-object/if_empty_object.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Empty object following `if` 3 | package regal.rules.bugs["if-empty-object"] 4 | 5 | import data.regal.capabilities 6 | import data.regal.result 7 | 8 | # METADATA 9 | # description: Missing capability for keyword `if` 10 | # custom: 11 | # severity: warning 12 | notices contains result.notice(rego.metadata.chain()) if not capabilities.has_if 13 | 14 | # METADATA 15 | # description: | 16 | # NOTE: this rule has been deprecated and is no longer enabled by default 17 | # Use the `if-object-literal` rule instead, which checks for any object, 18 | # non-empty or not 19 | report contains violation if { 20 | some rule in input.rules 21 | 22 | count(rule.body) == 1 23 | 24 | rule.body[0].terms.type == "object" 25 | rule.body[0].terms.value == [] 26 | 27 | violation := result.fail(rego.metadata.chain(), result.location(rule)) 28 | } 29 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/if-empty-object/if_empty_object_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["if-empty-object_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.bugs["if-empty-object"] as rule 7 | 8 | test_fail_if_empty_object if { 9 | r := rule.report with input as ast.policy("rule if {}") 10 | 11 | r == {{ 12 | "category": "bugs", 13 | "description": "Empty object following `if`", 14 | "level": "error", 15 | "location": { 16 | "col": 1, 17 | "file": "policy.rego", 18 | "row": 3, 19 | "text": "rule if {}", 20 | "end": { 21 | "col": 11, 22 | "row": 3, 23 | }, 24 | }, 25 | "related_resources": [{ 26 | "description": "documentation", 27 | "ref": config.docs.resolve_url("$baseUrl/$category/if-empty-object", "bugs"), 28 | }], 29 | "title": "if-empty-object", 30 | }} 31 | } 32 | 33 | test_success_if_non_empty_object if { 34 | # this is arguably just as useless, but we'll defer 35 | # to the constant-condition rule for these cases 36 | r := rule.report with input as ast.policy(`rule if {"foo": "bar"}`) 37 | 38 | r == set() 39 | } 40 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/if-object-literal/if_object_literal.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Object literal following `if` 3 | package regal.rules.bugs["if-object-literal"] 4 | 5 | import data.regal.capabilities 6 | import data.regal.result 7 | 8 | # METADATA 9 | # description: Missing capability for keyword `if` 10 | # custom: 11 | # severity: warning 12 | notices contains result.notice(rego.metadata.chain()) if not capabilities.has_if 13 | 14 | report contains violation if { 15 | some rule in input.rules 16 | 17 | count(rule.body) == 1 18 | rule.body[0].terms.type == "object" 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(rule.body[0].terms)) 21 | } 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/if-object-literal/if_object_literal_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["if-object-literal_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.bugs["if-object-literal"] as rule 7 | 8 | test_fail[name] if { 9 | some name, [policy, location] in { 10 | "empty_object": [ 11 | `rule if {}`, 12 | { 13 | "col": 9, 14 | "row": 3, 15 | "text": "rule if {}", 16 | "end": { 17 | "col": 11, 18 | "row": 3, 19 | }, 20 | }, 21 | ], 22 | "non_empty_object": [ 23 | `rule if {"x": input.x}`, 24 | { 25 | "col": 9, 26 | "row": 3, 27 | "text": `rule if {"x": input.x}`, 28 | "end": { 29 | "col": 23, 30 | "row": 3, 31 | }, 32 | }, 33 | ], 34 | } 35 | r := rule.report with input as ast.policy(policy) 36 | 37 | r == {{ 38 | "category": "bugs", 39 | "description": "Object literal following `if`", 40 | "level": "error", 41 | "location": object.union({"file": "policy.rego"}, location), 42 | "related_resources": [{ 43 | "description": "documentation", 44 | "ref": config.docs.resolve_url("$baseUrl/$category/if-object-literal", "bugs"), 45 | }], 46 | "title": "if-object-literal", 47 | }} 48 | } 49 | 50 | test_success_not_an_object if { 51 | r := rule.report with input as ast.with_rego_v1(`rule if { true }`) 52 | 53 | r == set() 54 | } 55 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/import-shadows-rule/import_shadows_rule.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Import shadows rule 3 | package regal.rules.bugs["import-shadows-rule"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | head := input.rules[_].head 10 | 11 | count(head.ref) == 1 12 | head.ref[0].value in ast.imported_identifiers 13 | 14 | violation := result.fail(rego.metadata.chain(), result.location(head)) 15 | } 16 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/internal-entrypoint/internal_entrypoint.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Entrypoint can't be marked internal 3 | package regal.rules.bugs["internal-entrypoint"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in ast.rules 10 | some annotation in rule.annotations 11 | 12 | annotation.entrypoint == true 13 | 14 | some i, part in rule.head.ref 15 | 16 | _any_internal(i, part) 17 | 18 | violation := result.fail(rego.metadata.chain(), result.location(part)) 19 | } 20 | 21 | _any_internal(0, part) if startswith(part.value, "_") 22 | 23 | _any_internal(_, part) if { 24 | part.type == "string" 25 | startswith(part.value, "_") 26 | } 27 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/invalid-metadata-attribute/invalid_metadata_attribute.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Invalid attribute in metadata annotation 3 | package regal.rules.bugs["invalid-metadata-attribute"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some block in ast.comments.blocks 10 | 11 | regex.match(`^\s*METADATA`, block[0].text) 12 | 13 | some attribute in object.keys(yaml.unmarshal(concat("\n", [entry.text | 14 | some i, entry in block 15 | i > 0 16 | ]))) 17 | 18 | not attribute in ast.comments.metadata_attributes 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location([line | 21 | some line in block 22 | startswith(trim_space(line.text), concat("", [attribute, ":"])) 23 | ][0])) 24 | } 25 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/invalid-metadata-attribute/invalid_metadata_attribute_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["invalid-metadata-attribute_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | import data.regal.rules.bugs["invalid-metadata-attribute"] as rule 6 | 7 | test_fail_invalid_attribute if { 8 | r := rule.report with input as ast.policy(` 9 | # METADATA 10 | # title: allow 11 | # is_true: yes 12 | allow := true 13 | `) 14 | 15 | r == {{ 16 | "category": "bugs", 17 | "description": "Invalid attribute in metadata annotation", 18 | "level": "error", 19 | "location": { 20 | "col": 1, 21 | "file": "policy.rego", 22 | "row": 6, 23 | "end": { 24 | "col": 15, 25 | "row": 6, 26 | }, 27 | "text": "# is_true: yes", 28 | }, 29 | "related_resources": [{ 30 | "description": "documentation", 31 | "ref": config.docs.resolve_url("$baseUrl/$category/invalid-metadata-attribute", "bugs"), 32 | }], 33 | "title": "invalid-metadata-attribute", 34 | }} 35 | } 36 | 37 | test_success_valid_metadata if { 38 | r := rule.report with input as ast.policy(` 39 | # METADATA 40 | # title: valid 41 | # description: also valid 42 | allow := true 43 | `) 44 | 45 | r == set() 46 | } 47 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/leaked-internal-reference/leaked_internal_reference.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Outside reference to internal rule or function 3 | package regal.rules.bugs["leaked-internal-reference"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | _enabled(_valid_test_file_name(input.regal.file.name), _enable_for_test_files) 11 | 12 | value := ast.found.refs[_][_].value 13 | 14 | contains(ast.ref_to_string(value), "._") 15 | 16 | violation := result.fail(rego.metadata.chain(), result.ranged_from_ref(value)) 17 | } 18 | 19 | report contains violation if { 20 | _enabled(_valid_test_file_name(input.regal.file.name), _enable_for_test_files) 21 | 22 | value := input.imports[_].path.value 23 | 24 | contains(ast.ref_to_string(value), "._") 25 | 26 | violation := result.fail(rego.metadata.chain(), result.ranged_from_ref(value)) 27 | } 28 | 29 | default _enabled(_, _) := true 30 | 31 | _enabled(true, false) := false 32 | 33 | default _valid_test_file_name(_) := false 34 | 35 | _valid_test_file_name(filename) if endswith(filename, "_test.rego") 36 | _valid_test_file_name("test.rego") # Styra DAS convention considered OK 37 | 38 | default _enable_for_test_files := false 39 | 40 | _enable_for_test_files := config.rules.bugs["leaked-internal-reference"]["include-test-files"] 41 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/not-equals-in-loop/not_equals_in_loop.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use of != in loop 3 | package regal.rules.bugs["not-equals-in-loop"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule_index, i 10 | ast.found.expressions[rule_index][i].terms[0].type == "ref" 11 | 12 | terms := ast.found.expressions[rule_index][i].terms 13 | 14 | terms[0].type == "ref" 15 | terms[0].value[0].type == "var" 16 | terms[0].value[0].value == "neq" 17 | 18 | some neq_term in array.slice(terms, 1, 100) 19 | neq_term.type == "ref" 20 | 21 | some value in neq_term.value 22 | ast.is_wildcard(value) 23 | 24 | violation := result.fail(rego.metadata.chain(), result.location(terms[0])) 25 | } 26 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/rule-assigns-default/rule_assigns_default.rego: -------------------------------------------------------------------------------- 1 | # regal eval:use-as-input 2 | # METADATA 3 | # description: Rule assigned its default value 4 | package regal.rules.bugs["rule-assigns-default"] 5 | 6 | import data.regal.ast 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | count(_default_rule_values) > 0 11 | 12 | some rule in input.rules 13 | 14 | not rule["default"] 15 | 16 | ref := ast.ref_to_string(rule.head.ref) 17 | 18 | _default_rule_values[ref] == rule.head.value.value 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(rule.head.value)) 21 | } 22 | 23 | _default_rule_values[ref] := rule.head.value.value if { 24 | some rule in input.rules 25 | rule["default"] 26 | 27 | ref := ast.ref_to_string(rule.head.ref) 28 | } 29 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/rule-assigns-default/rule_assigns_default_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["rule-assigns-default_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.bugs["rule-assigns-default"] as rule 7 | 8 | test_fail_rule_assigned_default_value if { 9 | r := rule.report with input as ast.with_rego_v1(` 10 | 11 | default allow := false 12 | 13 | allow := false if { 14 | some conditions in policy 15 | } 16 | `) 17 | 18 | r == {{ 19 | "category": "bugs", 20 | "description": "Rule assigned its default value", 21 | "level": "error", 22 | "location": { 23 | "col": 11, 24 | "end": { 25 | "col": 16, 26 | "row": 9, 27 | }, 28 | "file": "policy.rego", 29 | "row": 9, 30 | "text": "\tallow := false if {", 31 | }, 32 | "related_resources": [{ 33 | "description": "documentation", 34 | "ref": config.docs.resolve_url("$baseUrl/$category/rule-assigns-default", "bugs"), 35 | }], 36 | "title": "rule-assigns-default", 37 | }} 38 | } 39 | 40 | test_success_rule_not_assigned_default_value if { 41 | module := ast.with_rego_v1(` 42 | 43 | default allow := false 44 | 45 | allow := true if { 46 | some conditions in policy 47 | } 48 | `) 49 | r := rule.report with input as module 50 | 51 | r == set() 52 | } 53 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/rule-named-if/rule_named_if.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Rule named "if" 3 | package regal.rules.bugs["rule-named-if"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.result 8 | 9 | # METADATA 10 | # description: Since OPA 1.0, rule-named-if enabled only when provided a v0 policy 11 | # custom: 12 | # severity: none 13 | notices contains result.notice(rego.metadata.chain()) if { 14 | capabilities.is_opa_v1 15 | input.regal.file.rego_version != "v0" 16 | } 17 | 18 | report contains violation if { 19 | # this is only an optimization — as we already have collected all the rule 20 | # names, we'll do a fast lookup to know if we need to iterate over the rules 21 | # at all, which we'll do only to retrieve the location of the rule 22 | "if" in ast.rule_names 23 | 24 | some rule in input.rules 25 | ast.ref_to_string(rule.head.ref) == "if" 26 | 27 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 28 | } 29 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/rule-named-if/rule_named_if_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.bugs["rule-named-if_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.capabilities 5 | import data.regal.config 6 | import data.regal.rules.bugs["rule-named-if"] as rule 7 | 8 | test_fail_rule_named_if if { 9 | r := rule.report with input as ast.with_rego_v0(` 10 | allow := true if { 11 | input.foo 12 | }`) 13 | 14 | r == {{ 15 | "category": "bugs", 16 | "description": "Rule named \"if\"", 17 | "level": "error", 18 | "location": { 19 | "col": 16, 20 | "file": "policy_v0.rego", 21 | "row": 4, 22 | "end": { 23 | "col": 18, 24 | "row": 4, 25 | }, 26 | "text": "\tallow := true if {", 27 | }, 28 | "related_resources": [{ 29 | "description": "documentation", 30 | "ref": config.docs.resolve_url("$baseUrl/$category/rule-named-if", "bugs"), 31 | }], 32 | "title": "rule-named-if", 33 | }} with input.regal.file.rego_version as "v0" 34 | with capabilities.is_opa_v1 as false 35 | } 36 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/rule-shadows-builtin/rule_shadows_builtin.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Rule name shadows built-in 3 | package regal.rules.bugs["rule-shadows-builtin"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | head := input.rules[_].head 10 | 11 | ast.ref_to_string(head.ref) in ast.builtin_namespaces 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(head)) 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/time-now-ns-twice/time_now_ns_twice.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Repeated calls to `time.now_ns` 3 | package regal.rules.bugs["time-now-ns-twice"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule_index 10 | ast.function_calls[rule_index][_].name == "time.now_ns" 11 | 12 | time_now_calls := [call | 13 | some call in ast.function_calls[rule_index] 14 | call.name == "time.now_ns" 15 | ] 16 | 17 | some i, repeated in time_now_calls 18 | i > 0 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(repeated)) 21 | } 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/top-level-iteration/top_level_iteration.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Iteration in top-level assignment 3 | package regal.rules.bugs["top-level-iteration"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some i 10 | input.rules[i].head.value.type == "ref" 11 | rule := input.rules[i] 12 | 13 | # skip if vars in the ref head 14 | count([part | 15 | some i, part in rule.head.ref 16 | i > 0 17 | part.type == "var" 18 | ]) == 0 19 | 20 | some part in array.slice(rule.head.value.value, 1, 100) 21 | 22 | part.type == "var" 23 | 24 | _illegal_value_ref(part.value, rule, ast.identifiers) 25 | 26 | # this is expensive, but the preconditions should ensure that 27 | # very few rules evaluate this far 28 | not _var_in_body(rule, part.value) 29 | 30 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 31 | } 32 | 33 | _var_in_body(rule, value) if { 34 | walk(rule.body, [_, node]) 35 | node.type == "var" 36 | node.value == value 37 | } 38 | 39 | _illegal_value_ref(value, rule, identifiers) if { 40 | not value in identifiers 41 | not _is_arg_or_input(value, rule) 42 | } 43 | 44 | _is_arg_or_input(value, rule) if value in ast.function_arg_names(rule) 45 | _is_arg_or_input(value, _) if value[0].value == "input" 46 | _is_arg_or_input("input", _) 47 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/unassigned-return-value/unassigned_return_value.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Non-boolean return value unassigned 3 | package regal.rules.bugs["unassigned-return-value"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | terms := ast.found.expressions[_][_].terms 11 | 12 | terms[0].type == "ref" 13 | terms[0].value[0].type == "var" 14 | 15 | ref_name := terms[0].value[0].value 16 | ref_name in ast.builtin_names 17 | 18 | # special case as the "result" of print is "" 19 | ref_name != "print" 20 | 21 | config.capabilities.builtins[ref_name].decl.result != "boolean" 22 | 23 | # no violation if the return value is declared as the last function argument 24 | # see the function-arg-return rule for *that* violation 25 | not ast.function_ret_in_args(ref_name, terms) 26 | 27 | violation := result.fail(rego.metadata.chain(), result.location(terms[0])) 28 | } 29 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/var-shadows-builtin/var_shadows_builtin.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Variable name shadows built-in 3 | package regal.rules.bugs["var-shadows-builtin"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | var := ast.found.vars[_][_][_] 10 | 11 | var.value in ast.builtin_namespaces 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(var)) 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/rules/bugs/zero-arity-function/zero_arity_function.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid functions without args 3 | package regal.rules.bugs["zero-arity-function"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | # notably, not ast.functions, as zero-arity functions are treated 11 | # as regular rules (i.e. they have no `args` key in the head) 12 | head := ast.rules[_].head 13 | 14 | text := util.to_location_object(head.location).text 15 | 16 | regex.match(`^[a-zA-z1-9_\.\[\]"]+\(\)`, text) 17 | 18 | violation := result.fail(rego.metadata.chain(), result.ranged_from_ref(head.ref)) 19 | } 20 | -------------------------------------------------------------------------------- /bundle/regal/rules/custom/forbidden-function-call/forbidden_function_call.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Forbidden function call 3 | package regal.rules.custom["forbidden-function-call"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | import data.regal.util 9 | 10 | report contains violation if { 11 | forbidden := util.to_set(config.rules.custom["forbidden-function-call"]["forbidden-functions"]) 12 | 13 | # avoid traversal if no forbidden function is called 14 | util.intersects(forbidden, ast.builtin_functions_called) 15 | 16 | ref := ast.found.calls[_][_] 17 | name := ast.ref_to_string(ref[0].value) 18 | name in forbidden 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(ref)) 21 | } 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/custom/forbidden-function-call/forbidden_function_call_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.custom["forbidden-function-call_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.capabilities 5 | import data.regal.config 6 | 7 | import data.regal.rules.custom["forbidden-function-call"] as rule 8 | 9 | test_fail_forbidden_function if { 10 | module := ast.policy(`foo := http.send({"method": "GET", "url": "https://example.com"})`) 11 | 12 | r := rule.report with input as module with config.rules as {"custom": {"forbidden-function-call": { 13 | "level": "error", 14 | "forbidden-functions": ["http.send"], 15 | }}} 16 | with config.capabilities as capabilities.provided 17 | 18 | r == {{ 19 | "category": "custom", 20 | "description": "Forbidden function call", 21 | "level": "error", 22 | "location": { 23 | "col": 8, 24 | "file": "policy.rego", 25 | "row": 3, 26 | "end": { 27 | "col": 17, 28 | "row": 3, 29 | }, 30 | "text": `foo := http.send({"method": "GET", "url": "https://example.com"})`, 31 | }, 32 | "related_resources": [{ 33 | "description": "documentation", 34 | "ref": config.docs.resolve_url("$baseUrl/$category/forbidden-function-call", "custom"), 35 | }], 36 | "title": "forbidden-function-call", 37 | }} 38 | } 39 | -------------------------------------------------------------------------------- /bundle/regal/rules/custom/prefer-value-in-head/prefer_value_in_head.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Prefer value in rule head 3 | package regal.rules.custom["prefer-value-in-head"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | some rule in input.rules 11 | 12 | var := _var_in_head(rule.head) 13 | terms := regal.last(rule.body).terms 14 | 15 | terms[0].value[0].type == "var" 16 | terms[0].value[0].value in {"eq", "assign"} 17 | terms[1].type == "var" 18 | terms[1].value == var 19 | 20 | not _scalar_fail(terms[2].type, ast.scalar_types) 21 | not _excepted_var_name(var) 22 | 23 | violation := result.fail(rego.metadata.chain(), result.location(terms[2])) 24 | } 25 | 26 | _var_in_head(head) := head.value.value if head.value.type == "var" 27 | 28 | _var_in_head(head) := head.key.value if { 29 | not head.value 30 | head.key.type == "var" 31 | } 32 | 33 | _scalar_fail(term_type, scalar_types) if { 34 | config.rules.custom["prefer-value-in-head"]["only-scalars"] == true 35 | not term_type in scalar_types 36 | } 37 | 38 | _excepted_var_name(name) if name in config.rules.custom["prefer-value-in-head"]["except-var-names"] 39 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/boolean-assignment/boolean_assignment.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Prefer `if` over boolean assignment 3 | package regal.rules.idiomatic["boolean-assignment"] 4 | 5 | import data.regal.config 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in input.rules 10 | 11 | rhv := rule.head.value 12 | 13 | rhv.type == "call" 14 | rhv.value[0].type == "ref" 15 | rhv.value[0].value[0].type == "var" 16 | 17 | ref_name := rhv.value[0].value[0].value 18 | 19 | config.capabilities.builtins[ref_name].decl.result == "boolean" 20 | 21 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 22 | } 23 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/custom-in-construct/custom_in_construct.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Custom function may be replaced by `in` keyword 3 | package regal.rules.idiomatic["custom-in-construct"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in ast.functions 10 | 11 | # while there could be more convoluted ways of doing this 12 | # we'll settle for the likely most common case (`item == coll[_]`) 13 | count(rule.body) == 1 14 | 15 | terms := rule.body[0].terms 16 | 17 | terms[0].value[0].type == "var" 18 | terms[0].value[0].value in {"eq", "equal"} 19 | 20 | [var, ref] := _normalize_eq_terms(terms) 21 | 22 | arg_names := ast.function_arg_names(rule) 23 | 24 | var.value in arg_names 25 | ref.value[0].value in arg_names 26 | ast.is_wildcard(ref.value[1]) 27 | 28 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 29 | } 30 | 31 | # normalize var to always always be on the left hand side 32 | _normalize_eq_terms(terms) := [terms[1], terms[2]] if { 33 | terms[1].type == "var" 34 | terms[2].type == "ref" 35 | terms[2].value[0].type == "var" 36 | } 37 | 38 | _normalize_eq_terms(terms) := [terms[2], terms[1]] if { 39 | terms[1].type == "ref" 40 | terms[1].value[0].type == "var" 41 | terms[2].type == "var" 42 | } 43 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/in-wildcard-key/in_wildcard_key.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Unnecessary wildcard key 3 | package regal.rules.idiomatic["in-wildcard-key"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | # some _, v in input 9 | report contains violation if { 10 | some symbols 11 | ast.found.symbols[_][symbols][0].type == "call" 12 | 13 | symbol := symbols[0] 14 | 15 | symbol.value[0].value[0].type == "var" 16 | symbol.value[0].value[1].type == "string" 17 | symbol.value[0].value[0].value == "internal" 18 | symbol.value[0].value[1].value == "member_3" 19 | symbol.value[1].type == "var" 20 | startswith(symbol.value[1].value, "$") 21 | 22 | violation := result.fail(rego.metadata.chain(), result.location(symbol.value[1])) 23 | } 24 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/in-wildcard-key/in_wildcard_key_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.idiomatic["in-wildcard-key_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.idiomatic["in-wildcard-key"] as rule 7 | 8 | test_fail_wildcard_key_not_needed if { 9 | r := rule.report with input as ast.policy("r if some _, v in input") 10 | 11 | r == {{ 12 | "category": "idiomatic", 13 | "description": "Unnecessary wildcard key", 14 | "level": "error", 15 | "location": { 16 | "col": 11, 17 | "end": { 18 | "col": 12, 19 | "row": 3, 20 | }, 21 | "file": "policy.rego", 22 | "row": 3, 23 | "text": "r if some _, v in input", 24 | }, 25 | "related_resources": [{ 26 | "description": "documentation", 27 | "ref": config.docs.resolve_url("$baseUrl/$category/in-wildcard-key", "idiomatic"), 28 | }], 29 | "title": "in-wildcard-key", 30 | }} 31 | } 32 | 33 | test_success[case] if { 34 | some case in [ 35 | "r if some v in input", 36 | "r if some k, _ in input", 37 | "r if some [k], v in input", 38 | "r if some [_, k], v in input", 39 | ] 40 | 41 | r := rule.report with input as ast.policy(case) 42 | r == set() 43 | } 44 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/no-defined-entrypoint/no_defined_entrypoint.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Missing entrypoint annotation 3 | package regal.rules.idiomatic["no-defined-entrypoint"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | # METADATA 10 | # description: | 11 | # collects `entrypoint: true` annotations from any given module 12 | aggregate contains entry if { 13 | some annotation in ast.annotations 14 | annotation.entrypoint == true 15 | 16 | entry := result.aggregate(rego.metadata.chain(), {"entrypoint": util.to_location_object(annotation.location)}) 17 | } 18 | 19 | # METADATA 20 | # schemas: 21 | # - input: schema.regal.aggregate 22 | aggregate_report contains violation if { 23 | count(input.aggregate) == 0 24 | 25 | violation := result.fail(rego.metadata.chain(), {}) 26 | } 27 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/single-item-in/single_item_in.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid `in` for single item collection 3 | package regal.rules.idiomatic["single-item-in"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | call := ast.found.calls[_][_] 10 | 11 | call[0].value[0].value == "internal" 12 | call[0].value[1].value == "member_2" 13 | 14 | call[2].type in {"array", "set", "object"} 15 | count(call[2].value) == 1 16 | 17 | violation := result.fail(rego.metadata.chain(), result.location(call)) 18 | } 19 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-contains/use_contains.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use the `contains` keyword 3 | package regal.rules.idiomatic["use-contains"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.result 8 | import data.regal.util 9 | 10 | # METADATA 11 | # description: Missing capability for keyword `contains` 12 | # custom: 13 | # severity: warning 14 | notices contains result.notice(rego.metadata.chain()) if not capabilities.has_contains 15 | 16 | # METADATA 17 | # description: Since OPA 1.0, use-contains enabled only when provided a v0 policy 18 | # custom: 19 | # severity: none 20 | notices contains result.notice(rego.metadata.chain()) if { 21 | capabilities.is_opa_v1 22 | input.regal.file.rego_version != "v0" 23 | } 24 | 25 | report contains violation if { 26 | # if rego.v1 is imported, OPA will ensure this anyway 27 | not ast.imports_has_path(ast.imports, ["rego", "v1"]) 28 | 29 | some rule in ast.rules 30 | 31 | rule.head.key 32 | not rule.head.value 33 | 34 | text := split(util.to_location_object(rule.location).text, "\n")[0] 35 | 36 | not contains(text, " contains ") 37 | 38 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 39 | } 40 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-if/use_if.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use the `if` keyword 3 | package regal.rules.idiomatic["use-if"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.result 8 | import data.regal.util 9 | 10 | # METADATA 11 | # description: Missing capability for keyword `if` 12 | # custom: 13 | # severity: warning 14 | notices contains result.notice(rego.metadata.chain()) if not capabilities.has_if 15 | 16 | # METADATA 17 | # description: Since OPA 1.0, use-if enabled only when provided a v0 policy 18 | # custom: 19 | # severity: none 20 | notices contains result.notice(rego.metadata.chain()) if { 21 | capabilities.is_opa_v1 22 | input.regal.file.rego_version != "v0" 23 | } 24 | 25 | report contains violation if { 26 | # if rego.v1 is imported, OPA will ensure this anyway 27 | not ast.imports_has_path(ast.imports, ["rego", "v1"]) 28 | 29 | some rule in input.rules 30 | rule.body 31 | 32 | head_len := count(util.to_location_object(rule.head.location).text) 33 | text := trim_space(substring(util.to_location_object(rule.location).text, head_len, -1)) 34 | 35 | not startswith(text, "if") 36 | 37 | violation := result.fail(rego.metadata.chain(), result.ranged_from_ref(rule.head.ref)) 38 | } 39 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-if/use_if_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.idiomatic["use-if_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.idiomatic["use-if"] as rule 7 | 8 | test_fail_should_use_if if { 9 | module := ast.with_rego_v0(`rule := [true | 10 | input[_] 11 | ] { 12 | input.attribute 13 | }`) 14 | r := rule.report with input as module 15 | 16 | r == {{ 17 | "category": "idiomatic", 18 | "description": "Use the `if` keyword", 19 | "level": "error", 20 | "location": { 21 | "col": 1, 22 | "file": "policy_v0.rego", 23 | "row": 3, 24 | "end": { 25 | "col": 5, 26 | "row": 3, 27 | }, 28 | "text": "rule := [true |", 29 | }, 30 | "related_resources": [{ 31 | "description": "documentation", 32 | "ref": config.docs.resolve_url("$baseUrl/$category/use-if", "idiomatic"), 33 | }], 34 | "title": "use-if", 35 | }} 36 | } 37 | 38 | test_success_uses_if if { 39 | r := rule.report with input as ast.policy("rule := [true | input[_]] if input.attribute") 40 | 41 | r == set() 42 | } 43 | 44 | test_success_no_body_no_if if { 45 | r := rule.report with input as ast.policy(`rule := "without body"`) 46 | 47 | r == set() 48 | } 49 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-in-operator/use_in_operator.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use in to check for membership 3 | package regal.rules.idiomatic["use-in-operator"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | terms := input.rules[_].body[_].terms 10 | 11 | terms[0].type == "ref" 12 | terms[0].value[0].type == "var" 13 | terms[0].value[0].value in {"eq", "equal"} 14 | 15 | nl_terms := _non_loop_term(terms) 16 | count(nl_terms) == 1 17 | 18 | nlt := nl_terms[0] 19 | _static_term(nlt.term) 20 | 21 | # Use the non-loop term position to determine the 22 | # location of the loop term (3 is the count of terms) 23 | violation := result.fail(rego.metadata.chain(), result.location(terms[3 - nlt.pos])) 24 | } 25 | 26 | _non_loop_term(terms) := [{"pos": i + 1, "term": term} | 27 | some i, term in array.slice(terms, 1, 3) 28 | not _loop_term(term) 29 | ] 30 | 31 | _loop_term(term) if { 32 | term.type == "ref" 33 | term.value[0].type == "var" 34 | 35 | ast.is_wildcard(regal.last(term.value)) 36 | } 37 | 38 | _static_term(term) if term.type in {"array", "boolean", "object", "null", "number", "set", "string", "var"} 39 | 40 | _static_term(term) if { 41 | term.type == "ref" 42 | ast.static_ref(term) 43 | } 44 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-some-for-output-vars/use_some_for_output_vars.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use `some` to declare output variables 3 | package regal.rules.idiomatic["use-some-for-output-vars"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | some rule_index, i 11 | elem := ast.found.refs[rule_index][_].value[i] 12 | 13 | # first item can't be a loop or key ref 14 | i != 0 15 | elem.type == "var" 16 | not ast.is_wildcard(elem) 17 | 18 | rule := input.rules[to_number(rule_index)] 19 | 20 | not elem.value in ast.find_names_in_scope(rule, elem.location) 21 | 22 | path := _location_path(rule, elem.location) 23 | 24 | not _var_in_comprehension_body(elem, rule, path) 25 | 26 | violation := result.fail(rego.metadata.chain(), result.location(elem)) 27 | } 28 | 29 | _location_path(rule, location) := path if walk(rule, [path, location]) 30 | 31 | _var_in_comprehension_body(var, rule, path) if { 32 | some v in _comprehension_body_vars(rule, path) 33 | v.type == var.type 34 | v.value == var.value 35 | } 36 | 37 | _comprehension_body_vars(rule, path) := [vars | 38 | some parent_path in array.reverse(util.all_paths(path)) 39 | 40 | node := object.get(rule, parent_path, {}) 41 | 42 | node.type in {"arraycomprehension", "objectcomprehension", "setcomprehension"} 43 | 44 | vars := ast.find_vars(node.value.body) 45 | ][0] 46 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-strings-count/use_strings_count.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use `strings.count` where possible 3 | package regal.rules.idiomatic["use-strings-count"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.result 8 | 9 | # METADATA 10 | # description: Missing capability for built-in function `strings.count` 11 | # custom: 12 | # severity: warning 13 | notices contains result.notice(rego.metadata.chain()) if not capabilities.has_strings_count 14 | 15 | # METADATA 16 | # description: flag calls to `count` where the first argument is a call to `indexof_n` 17 | report contains violation if { 18 | ref := ast.found.calls[_][_] 19 | 20 | ref[0].value[0].type == "var" 21 | ref[0].value[0].value == "count" 22 | 23 | ref[1].type == "call" 24 | ref[1].value[0].value[0].type == "var" 25 | ref[1].value[0].value[0].value == "indexof_n" 26 | 27 | loc1 := result.location(ref[0]) 28 | loc2 := result.location(ref[1]) 29 | 30 | violation := result.fail(rego.metadata.chain(), result.ranged_location_between(loc1, loc2)) 31 | } 32 | -------------------------------------------------------------------------------- /bundle/regal/rules/idiomatic/use-strings-count/use_strings_count_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.idiomatic["use-strings-count_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.idiomatic["use-strings-count"] as rule 7 | 8 | test_fail_can_use_strings_count if { 9 | module := ast.with_rego_v1(`x := count(indexof_n("foo", "o"))`) 10 | 11 | r := rule.report with input as module 12 | r == {{ 13 | "category": "idiomatic", 14 | "description": "Use `strings.count` where possible", 15 | "level": "error", 16 | "location": { 17 | "col": 6, 18 | "file": "policy.rego", 19 | "row": 5, 20 | "text": `x := count(indexof_n("foo", "o"))`, 21 | "end": {"col": 34, "row": 5}, 22 | }, 23 | "related_resources": [{ 24 | "description": "documentation", 25 | "ref": config.docs.resolve_url("$baseUrl/$category/use-strings-count", "idiomatic"), 26 | }], 27 | "title": "use-strings-count", 28 | }} 29 | } 30 | 31 | test_has_notice_if_unmet_capability if { 32 | r := rule.notices with config.capabilities as {} 33 | r == {{ 34 | "category": "idiomatic", 35 | "description": "Missing capability for built-in function `strings.count`", 36 | "level": "notice", 37 | "severity": "warning", 38 | "title": "use-strings-count", 39 | }} 40 | } 41 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/avoid-importing-input/avoid_importing_input.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid importing input 3 | package regal.rules.imports["avoid-importing-input"] 4 | 5 | import data.regal.result 6 | 7 | report contains violation if { 8 | some imported in input.imports 9 | 10 | imported.path.value[0].value == "input" 11 | 12 | # Allow aliasing input, eg `import input as tfplan`: 13 | not _aliased_input(imported) 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(imported.path.value[0])) 16 | } 17 | 18 | _aliased_input(imported) if { 19 | count(imported.path.value) == 1 20 | imported.alias 21 | } 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/confusing-alias/confusing_alias.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Confusing alias of existing import 3 | package regal.rules.imports["confusing-alias"] 4 | 5 | import data.regal.result 6 | 7 | report contains violation if { 8 | count(_aliased_imports) > 0 9 | 10 | some aliased in _aliased_imports 11 | some imp in input.imports 12 | 13 | imp != aliased 14 | _paths_equal(aliased.path.value, imp.path.value) 15 | 16 | violation := result.fail(rego.metadata.chain(), result.location(aliased)) 17 | } 18 | 19 | _aliased_imports contains imp if { 20 | some imp in input.imports 21 | 22 | imp.alias 23 | } 24 | 25 | _paths_equal(p1, p2) if { 26 | count(p1) == count(p2) 27 | 28 | every i, part in p1 { 29 | part.type == p2[i].type 30 | part.value == p2[i].value 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/ignored-import/ignored_import.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Reference ignores import 3 | package regal.rules.imports["ignored-import"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | _import_paths contains path if { 9 | some imp in input.imports 10 | path := [p.value | some p in imp.path.value] 11 | 12 | path[0] in {"data", "input"} 13 | count(path) > 1 14 | } 15 | 16 | report contains violation if { 17 | ref := ast.found.refs[_][_] 18 | 19 | ref.value[0].type == "var" 20 | ref.value[0].value in {"data", "input"} 21 | 22 | most_specific_match := regal.last(sort([ip | 23 | ref_path := [p.value | some p in ref.value] 24 | 25 | some ip in _import_paths 26 | array.slice(ref_path, 0, count(ip)) == ip 27 | ])) 28 | 29 | violation := result.fail(rego.metadata.chain(), object.union( 30 | result.location(ref), 31 | {"description": sprintf("Reference ignores import of %s", [concat(".", most_specific_match)])}, 32 | )) 33 | } 34 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/implicit-future-keywords/implicit_future_keywords.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use explicit future keyword imports 3 | package regal.rules.imports["implicit-future-keywords"] 4 | 5 | import data.regal.config 6 | import data.regal.result 7 | 8 | # METADATA 9 | # description: Rule made obsolete by rego.v1 capability 10 | # custom: 11 | # severity: none 12 | notices contains result.notice(rego.metadata.chain()) if "rego_v1_import" in config.capabilities.features 13 | 14 | report contains violation if { 15 | some imported in input.imports 16 | 17 | imported.path.type == "ref" 18 | 19 | count(imported.path.value) == 2 20 | 21 | imported.path.value[0].type == "var" 22 | imported.path.value[0].value == "future" 23 | imported.path.value[1].type == "string" 24 | imported.path.value[1].value == "keywords" 25 | 26 | violation := result.fail(rego.metadata.chain(), result.location(imported.path.value[0])) 27 | } 28 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/import-after-rule/import_after_rule.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Import declared after rule 3 | package regal.rules.imports["import-after-rule"] 4 | 5 | import data.regal.result 6 | import data.regal.util 7 | 8 | report contains violation if { 9 | first_rule_row := util.to_location_object(input.rules[0].location).row 10 | 11 | some imp in input.imports 12 | 13 | util.to_location_object(imp.location).row > first_rule_row 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(imp)) 16 | } 17 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/import-after-rule/import_after_rule_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.imports["import-after-rule_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.imports["import-after-rule"] as rule 7 | 8 | test_fail_import_after_rule if { 9 | module := ast.policy(` 10 | rule := true 11 | 12 | import data.foo 13 | `) 14 | r := rule.report with input as module 15 | 16 | r == {{ 17 | "category": "imports", 18 | "description": "Import declared after rule", 19 | "level": "error", 20 | "location": { 21 | "col": 2, 22 | "file": "policy.rego", 23 | "row": 6, 24 | "end": { 25 | "col": 8, 26 | "row": 6, 27 | }, 28 | "text": "\timport data.foo", 29 | }, 30 | "related_resources": [{ 31 | "description": "documentation", 32 | "ref": config.docs.resolve_url("$baseUrl/$category/import-after-rule", "imports"), 33 | }], 34 | "title": "import-after-rule", 35 | }} 36 | } 37 | 38 | test_success_import_before_rule if { 39 | module := ast.policy(` 40 | import data.foo 41 | 42 | rule := true 43 | `) 44 | r := rule.report with input as module 45 | 46 | r == set() 47 | } 48 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/import-shadows-builtin/import_shadows_builtin.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Import shadows built-in namespace 3 | package regal.rules.imports["import-shadows-builtin"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some imp in input.imports 10 | 11 | imp.path.value[0].value in {"data", "input"} 12 | 13 | name := _significant_name(imp) 14 | name in ast.builtin_namespaces 15 | 16 | # AST quirk: while we'd ideally provide the location of the *path component*, 17 | # there is no location data provided for aliases. In order to be consistent, 18 | # we'll just provide the location of the import. 19 | violation := result.fail(rego.metadata.chain(), result.location(imp)) 20 | } 21 | 22 | _significant_name(imp) := imp.alias 23 | _significant_name(imp) := regal.last(imp.path.value).value if not imp.alias 24 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/import-shadows-import/import_shadows_import.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Import shadows another import 3 | package regal.rules.imports["import-shadows-import"] 4 | 5 | import data.regal.capabilities 6 | import data.regal.result 7 | 8 | # METADATA 9 | # description: Since OPA 1.0, import-shadows-import enabled only when provided a v0 policy, 10 | # custom: 11 | # severity: none 12 | notices contains result.notice(rego.metadata.chain()) if { 13 | capabilities.is_opa_v1 14 | input.regal.file.rego_version != "v0" 15 | } 16 | 17 | # regular import 18 | _ident(imported) := regal.last(imported.path.value).value if not imported.alias 19 | 20 | # aliased import 21 | _ident(imported) := imported.alias 22 | 23 | _identifiers := [_ident(imported) | some imported in input.imports] 24 | 25 | report contains violation if { 26 | some i, identifier in _identifiers 27 | 28 | identifier in array.slice(_identifiers, 0, i) 29 | 30 | violation := result.fail(rego.metadata.chain(), result.location(input.imports[i].path)) 31 | } 32 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/pointless-import/pointless_import.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Importing own package is pointless 3 | package regal.rules.imports["pointless-import"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | plen := count(input["package"].path) 10 | path := input.imports[_].path 11 | ilen := count(path.value) 12 | 13 | # allow package a.b to import a.b.c.d.e but not a.b or a.b.c 14 | ilen - plen < 2 15 | 16 | same := array.slice(path.value, 0, plen) 17 | 18 | ast.ref_value_equal(input["package"].path, same) 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(path)) 21 | } 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/redundant-alias/redundant_alias.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Redundant alias 3 | package regal.rules.imports["redundant-alias"] 4 | 5 | import data.regal.result 6 | 7 | report contains violation if { 8 | some imported in input.imports 9 | 10 | regal.last(imported.path.value).value == imported.alias 11 | 12 | violation := result.fail(rego.metadata.chain(), result.location(imported.path.value[0])) 13 | } 14 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/redundant-alias/redundant_alias_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.imports["redundant-alias_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | import data.regal.rules.imports["redundant-alias"] as rule 6 | 7 | test_fail_redundant_alias if { 8 | r := rule.report with input as ast.policy(`import data.foo.bar as bar`) 9 | 10 | r == {{ 11 | "category": "imports", 12 | "description": "Redundant alias", 13 | "related_resources": [{ 14 | "description": "documentation", 15 | "ref": config.docs.resolve_url("$baseUrl/$category/redundant-alias", "imports"), 16 | }], 17 | "title": "redundant-alias", 18 | "location": { 19 | "col": 8, 20 | "file": "policy.rego", 21 | "row": 3, 22 | "end": { 23 | "col": 12, 24 | "row": 3, 25 | }, 26 | "text": "import data.foo.bar as bar", 27 | }, 28 | "level": "error", 29 | }} 30 | } 31 | 32 | test_success_not_redundant_alias if { 33 | r := rule.report with input as ast.policy(`import data.foo.bar as valid`) 34 | 35 | r == set() 36 | } 37 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/redundant-data-import/redundant_data_import.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Redundant import of data 3 | package regal.rules.imports["redundant-data-import"] 4 | 5 | import data.regal.result 6 | 7 | report contains violation if { 8 | path := input.imports[_].path.value 9 | 10 | count(path) == 1 11 | path[0].value == "data" 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(path[0])) 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/use-rego-v1/use_rego_v1.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Use `import rego.v1` 3 | package regal.rules.imports["use-rego-v1"] 4 | 5 | import data.regal.ast 6 | import data.regal.capabilities 7 | import data.regal.result 8 | 9 | # METADATA 10 | # description: Missing capability for `import rego.v1` 11 | # custom: 12 | # severity: warning 13 | notices contains result.notice(rego.metadata.chain()) if { 14 | not capabilities.has_rego_v1_feature 15 | not capabilities.is_opa_v1 16 | } 17 | 18 | # METADATA 19 | # description: Since OPA 1.0, use-rego-v1 enabled only when provided a v0 policy 20 | # custom: 21 | # severity: none 22 | notices contains result.notice(rego.metadata.chain()) if { 23 | capabilities.is_opa_v1 24 | input.regal.file.rego_version != "v0" 25 | } 26 | 27 | report contains violation if { 28 | not ast.imports_has_path(ast.imports, ["rego", "v1"]) 29 | 30 | violation := result.fail(rego.metadata.chain(), result.location(input["package"])) 31 | } 32 | -------------------------------------------------------------------------------- /bundle/regal/rules/imports/use-rego-v1/use_rego_v1_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.imports["use-rego-v1_test"] 2 | 3 | import data.regal.capabilities 4 | import data.regal.config 5 | import data.regal.rules.imports["use-rego-v1"] as rule 6 | 7 | test_fail_missing_rego_v1_import if { 8 | r := rule.report with input as regal.parse_module("policy.rego", `package policy 9 | import future.keywords 10 | 11 | foo if not bar 12 | `) 13 | with config.capabilities as capabilities.provided 14 | 15 | r == {{ 16 | "category": "imports", 17 | "description": "Use `import rego.v1`", 18 | "related_resources": [{ 19 | "description": "documentation", 20 | "ref": config.docs.resolve_url("$baseUrl/$category/use-rego-v1", "imports"), 21 | }], 22 | "title": "use-rego-v1", 23 | "location": { 24 | "col": 1, 25 | "file": "policy.rego", 26 | "row": 1, 27 | "end": { 28 | "col": 8, 29 | "row": 1, 30 | }, 31 | "text": "package policy", 32 | }, 33 | "level": "error", 34 | }} 35 | } 36 | 37 | test_success_rego_v1_import if { 38 | r := rule.report with input as regal.parse_module("policy.rego", `package policy 39 | import rego.v1 40 | 41 | foo if not bar 42 | `) 43 | with config.capabilities as capabilities.provided 44 | r == set() 45 | } 46 | -------------------------------------------------------------------------------- /bundle/regal/rules/performance/with-outside-test-context/with_outside_test_context.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: '`with` used outside test context' 3 | package regal.rules.performance["with-outside-test-context"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some i, rule in input.rules 10 | some expr in ast.found.expressions[ast.rule_index_strings[i]] 11 | 12 | expr["with"] 13 | not strings.any_prefix_match(ast.ref_to_string(rule.head.ref), {"test_", "todo_test"}) 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(expr["with"][0])) 16 | } 17 | -------------------------------------------------------------------------------- /bundle/regal/rules/performance/with-outside-test-context/with_outside_test_context_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.performance["with-outside-test-context_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.performance["with-outside-test-context"] as rule 7 | 8 | test_fail_with_used_outside_test if { 9 | r := rule.report with input as ast.policy(` 10 | allow if { 11 | not foo.deny with input as {} 12 | } 13 | `) 14 | 15 | r == {{ 16 | "category": "performance", 17 | "description": "`with` used outside test context", 18 | "level": "error", 19 | "location": { 20 | "col": 16, 21 | "file": "policy.rego", 22 | "row": 5, 23 | "end": { 24 | "col": 32, 25 | "row": 5, 26 | }, 27 | "text": "\t\tnot foo.deny with input as {}", 28 | }, 29 | "related_resources": [{ 30 | "description": "documentation", 31 | "ref": config.docs.resolve_url("$baseUrl/$category/with-outside-test-context", "performance"), 32 | }], 33 | "title": "with-outside-test-context", 34 | }} 35 | } 36 | 37 | test_success_with_used_in_test if { 38 | r := rule.report with input as ast.policy("test_foo_deny if not foo.deny with input as {}") 39 | 40 | r == set() 41 | } 42 | 43 | test_success_with_used_in_todo_test if { 44 | r := rule.report with input as ast.policy("todo_test_foo_deny if not foo.deny with input as {}") 45 | 46 | r == set() 47 | } 48 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/avoid-get-and-list-prefix/avoid_get_and_list_prefix.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid `get_` and `list_` prefix for rules and functions 3 | package regal.rules.style["avoid-get-and-list-prefix"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in input.rules 10 | strings.any_prefix_match(ast.ref_to_string(rule.head.ref), {"get_", "list_"}) 11 | 12 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 13 | } 14 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/chained-rule-body/chained_rule_body.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid chaining rule bodies 3 | package regal.rules.style["chained-rule-body"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in input.rules 10 | 11 | ast.is_chained_rule_body(rule, input.regal.file.lines) 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/chained-rule-body/chained_rule_body_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.style["chained-rule-body_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.style["chained-rule-body"] as rule 7 | 8 | test_fail_chained_incremental_definition if { 9 | module := ast.policy(`rule if { 10 | input.x 11 | } { 12 | input.y 13 | }`) 14 | r := rule.report with input as module 15 | 16 | r == {{ 17 | "category": "style", 18 | "description": "Avoid chaining rule bodies", 19 | "level": "error", 20 | "location": { 21 | "col": 4, 22 | "file": "policy.rego", 23 | "row": 5, "text": "\t} {", 24 | "end": { 25 | "col": 3, 26 | "row": 7, 27 | }, 28 | }, 29 | "related_resources": [{ 30 | "description": "documentation", 31 | "ref": config.docs.resolve_url("$baseUrl/$category/chained-rule-body", "style"), 32 | }], 33 | "title": "chained-rule-body", 34 | }} 35 | } 36 | 37 | test_success_not_chained_incremental_definition if { 38 | module := ast.policy(` 39 | rule if { 40 | input.x 41 | } 42 | 43 | rule if { 44 | input.y 45 | }`) 46 | 47 | r := rule.report with input as module 48 | r == set() 49 | } 50 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/default-over-else/default_over_else.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Prefer default assignment over fallback else 3 | package regal.rules.style["default-over-else"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | some rule in _considered_rules 11 | 12 | # walking is expensive but necessary here, since there could be 13 | # any number of `else` clauses nested below. no need to traverse 14 | # the rule if there isn't a single `else` present though! 15 | walk(rule["else"], [_, value]) 16 | 17 | not value.body 18 | 19 | else_head := value.head 20 | 21 | ast.is_constant(else_head.value) 22 | 23 | violation := result.fail(rego.metadata.chain(), result.location(else_head)) 24 | } 25 | 26 | _considered_rules := input.rules if { 27 | config.rules.style["default-over-else"]["prefer-default-functions"] == true 28 | } else := ast.rules 29 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/default-over-not/default_over_not.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Prefer default assignment over negated condition 3 | package regal.rules.style["default-over-not"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some i, rule in ast.rules 10 | 11 | # part 1 — find unconditional static ref assignment 12 | # example: `rule := input.foo` 13 | 14 | not rule["default"] 15 | not rule.body 16 | 17 | ast.static_ref(rule.head.value) 18 | 19 | name := ast.ref_static_to_string(rule.head.ref) 20 | 21 | # part 2 - find corresponding assignment of constant on negated condition 22 | # example: `rule := 1 if not input.foo` 23 | 24 | sibling_rules := [sibling | 25 | some j, sibling in ast.rules 26 | i != j 27 | ast.ref_static_to_string(sibling.head.ref) == name 28 | ] 29 | 30 | some sibling in sibling_rules 31 | 32 | ast.is_constant(sibling.head.value) 33 | count(sibling.body) == 1 34 | sibling.body[0].negated 35 | 36 | ast.ref_value_equal(sibling.body[0].terms.value, rule.head.value.value) 37 | 38 | violation := result.fail(rego.metadata.chain(), result.location(sibling.body[0])) 39 | } 40 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/default-over-not/default_over_not_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.style["default-over-not_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.style["default-over-not"] as rule 7 | 8 | test_fail_default_over_not if { 9 | r := rule.report with input as ast.with_rego_v1(` 10 | user := input.user 11 | user := "foo" if not input.user 12 | `) 13 | 14 | r == {{ 15 | "category": "style", 16 | "description": "Prefer default assignment over negated condition", 17 | "level": "error", 18 | "location": { 19 | "col": 19, 20 | "file": "policy.rego", 21 | "row": 7, 22 | "end": { 23 | "col": 33, 24 | "row": 7, 25 | }, 26 | "text": "\tuser := \"foo\" if not input.user", 27 | }, 28 | "related_resources": [{ 29 | "description": "documentation", 30 | "ref": config.docs.resolve_url("$baseUrl/$category/default-over-not", "style"), 31 | }], 32 | "title": "default-over-not", 33 | }} 34 | } 35 | 36 | test_success_non_constant_value if { 37 | r := rule.report with input as ast.policy(` 38 | user := input.user 39 | user := var if not input.user 40 | `) 41 | 42 | r == set() 43 | } 44 | 45 | test_success_var_in_ref if { 46 | r := rule.report with input as ast.policy(` 47 | user := input[x].user 48 | user := "foo" if not input[x].user 49 | `) 50 | 51 | r == set() 52 | } 53 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/double-negative/double_negative.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid double negatives 3 | # related_resources: 4 | # - description: documentation 5 | # ref: https://docs.styra.com/regal/rules/style/double-negative 6 | # schemas: 7 | # - input: schema.regal.ast 8 | package regal.rules.style["double-negative"] 9 | 10 | import data.regal.ast 11 | import data.regal.result 12 | 13 | report contains violation if { 14 | some node, i 15 | ast.found.expressions[i][node].negated 16 | ast.found.expressions[i][node].terms.type == "var" 17 | 18 | strings.any_prefix_match(node.terms.value, { 19 | "cannot_", 20 | "no_", 21 | "non_", 22 | "not_", 23 | }) 24 | 25 | violation := result.fail(rego.metadata.chain(), result.location(node)) 26 | } 27 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/double-negative/double_negative_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.style["double-negative_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.style["double-negative"] as rule 7 | 8 | test_fail_double_negative if { 9 | r := rule.report with input as ast.policy(` 10 | import future.keywords.if 11 | 12 | not_fine := true 13 | 14 | fine if not not_fine 15 | `) 16 | 17 | r == {{ 18 | "category": "style", 19 | "description": "Avoid double negatives", 20 | "location": { 21 | "col": 13, 22 | "file": "policy.rego", 23 | "row": 8, "text": " fine if not not_fine", 24 | "end": { 25 | "col": 25, 26 | "row": 8, 27 | }, 28 | }, 29 | "related_resources": [{ 30 | "description": "documentation", 31 | "ref": config.docs.resolve_url("$baseUrl/$category/double-negative", "style"), 32 | }], 33 | "title": "double-negative", 34 | "level": "error", 35 | }} 36 | } 37 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/file-length/file_length.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Max file length exceeded 3 | package regal.rules.style["file-length"] 4 | 5 | import data.regal.config 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | count(input.regal.file.lines) > config.rules.style["file-length"]["max-file-length"] 10 | 11 | violation := result.fail(rego.metadata.chain(), result.location(input["package"])) 12 | } 13 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/file-length/file_length_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.style["file-length_test"] 2 | 3 | import data.regal.config 4 | 5 | import data.regal.rules.style["file-length"] as rule 6 | 7 | test_fail_configured_file_length_exceeded if { 8 | module := regal.parse_module("policy.rego", `package policy 9 | 10 | rule1 := "foo" 11 | rule2 := "bar" 12 | `) 13 | 14 | r := rule.report with input as module with config.rules as {"style": {"file-length": {"max-file-length": 2}}} 15 | 16 | r == {{ 17 | "category": "style", 18 | "description": "Max file length exceeded", 19 | "level": "error", 20 | "location": { 21 | "col": 1, 22 | "row": 1, 23 | "end": { 24 | "col": 8, 25 | "row": 1, 26 | }, 27 | "file": "policy.rego", 28 | "text": "package policy", 29 | }, 30 | "related_resources": [{ 31 | "description": "documentation", 32 | "ref": config.docs.resolve_url("$baseUrl/$category/file-length", "style"), 33 | }], 34 | "title": "file-length", 35 | }} 36 | } 37 | 38 | test_success_configured_file_length_within_limit if { 39 | module := regal.parse_module("policy.rego", `package policy 40 | 41 | rule1 := "foo" 42 | rule2 := "bar" 43 | `) 44 | 45 | r := rule.report with input as module with config.rules as {"style": {"file-length": {"max-file-length": 10}}} 46 | r == set() 47 | } 48 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/function-arg-return/function_arg_return.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Function argument used for return value 3 | package regal.rules.style["function-arg-return"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | included_functions := ast.all_function_names - _excluded_functions 11 | 12 | some fn 13 | ast.function_calls[_][fn].name in included_functions 14 | 15 | count(fn.args) > count(ast.all_functions[fn.name].decl.args) 16 | 17 | violation := result.fail(rego.metadata.chain(), result.location(regal.last(fn.args))) 18 | } 19 | 20 | _excluded_functions contains "print" 21 | _excluded_functions contains name if some name in config.rules.style["function-arg-return"]["except-functions"] 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/line-length/line_length.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Line too long 3 | package regal.rules.style["line-length"] 4 | 5 | import data.regal.config 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | max_line_length := object.get(config.rules, ["style", "line-length", "max-line-length"], 120) 10 | 11 | some i, line in input.regal.file.lines 12 | 13 | line != "" 14 | 15 | line_length := count(line) 16 | line_length > max_line_length 17 | 18 | not _has_word_above_threshold(line) 19 | 20 | violation := result.fail( 21 | rego.metadata.chain(), 22 | {"location": { 23 | "file": input.regal.file.name, 24 | "row": i + 1, 25 | "col": 1, 26 | "text": input.regal.file.lines[i], 27 | "end": { 28 | "row": i + 1, 29 | "col": line_length, 30 | }, 31 | }}, 32 | ) 33 | } 34 | 35 | _has_word_above_threshold(line) if { 36 | threshold := config.rules.style["line-length"]["non-breakable-word-threshold"] 37 | 38 | some word in split(line, " ") 39 | count(word) > threshold 40 | } 41 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/messy-rule/messy_rule.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Messy incremental rule 3 | package regal.rules.style["messy-rule"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some i, rule1 in input.rules 10 | 11 | cur_name := ast.ref_static_to_string(rule1.head.ref) 12 | 13 | # tests aren't really incremental rules, and other rules 14 | # will flag multiple rules with the same name 15 | not startswith(cur_name, "test_") 16 | not startswith(cur_name, "todo_test_") 17 | 18 | some j, rule2 in input.rules 19 | 20 | j > i 21 | 22 | nxt_name := ast.ref_static_to_string(rule2.head.ref) 23 | cur_name == nxt_name 24 | 25 | previous_name := ast.ref_static_to_string(input.rules[j - 1].head.ref) 26 | previous_name != nxt_name 27 | 28 | violation := result.fail(rego.metadata.chain(), result.location(rule2)) 29 | } 30 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/mixed-iteration/mixed_iteration.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Mixed iteration style 3 | package regal.rules.style["mixed-iteration"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule_index, symbol 10 | ast.found.symbols[rule_index][symbol][0].type == "call" 11 | 12 | last := regal.last(symbol[0].value) 13 | last.type == "ref" 14 | 15 | some i, term in last.value 16 | 17 | i > 0 18 | term.type == "var" 19 | ast.is_output_var(input.rules[to_number(rule_index)], term) 20 | 21 | violation := result.fail(rego.metadata.chain(), result.location(last)) 22 | } 23 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/no-whitespace-comment/no_whitespace_comment.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Comment should start with whitespace 3 | package regal.rules.style["no-whitespace-comment"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | report contains violation if { 10 | some comment in ast.comments_decoded 11 | 12 | not regex.match(`^[\s#]*$|^#*[\s]+.*$`, comment.text) 13 | not _excepted(comment.text) 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(comment)) 16 | } 17 | 18 | _excepted(text) if regex.match(config.rules.style["no-whitespace-comment"]["except-pattern"], text) 19 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/opa-fmt/opa_fmt.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: File should be formatted with `opa fmt` 3 | package regal.rules.style["opa-fmt"] 4 | 5 | import data.regal.result 6 | 7 | report contains violation if { 8 | # NOTE: 9 | # 1. this won't identify CRLF line endings, as we've stripped them from the input previously 10 | # 2. this will perform worse than having the text representation of the file in the input 11 | not regal.is_formatted(concat("\n", input.regal.file.lines), {"rego_version": input.regal.file.rego_version}) 12 | 13 | violation := result.fail(rego.metadata.chain(), {"location": { 14 | "file": input.regal.file.name, 15 | "row": 1, 16 | "col": 1, 17 | "text": input.regal.file.lines[0], 18 | }}) 19 | } 20 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/pointless-reassignment/pointless_reassignment.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Pointless reassignment of variable 3 | package regal.rules.style["pointless-reassignment"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | # pointless reassignment in rule head 9 | report contains violation if { 10 | some rule in ast.rules 11 | 12 | not rule.body 13 | 14 | rule.head.value.type == "var" 15 | count(rule.head.ref) == 1 16 | 17 | violation := result.fail(rego.metadata.chain(), result.location(rule)) 18 | } 19 | 20 | # pointless reassignment in rule body 21 | report contains violation if { 22 | expr := input.rules[_].body[_] 23 | 24 | not expr["with"] 25 | 26 | [lhs, rhs] := ast.assignment_terms(expr.terms) 27 | 28 | lhs.type == "var" 29 | rhs.type == "var" 30 | 31 | violation := result.fail(rego.metadata.chain(), result.infix_expr_location(expr.terms)) 32 | } 33 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/prefer-snake-case/prefer_snake_case.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Prefer snake_case for names 3 | package regal.rules.style["prefer-snake-case"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | some part in input["package"].path 11 | 12 | not util.is_snake_case(part.value) 13 | 14 | violation := result.fail(rego.metadata.chain(), result.location(part)) 15 | } 16 | 17 | report contains violation if { 18 | some rule in input.rules 19 | some part in ast.named_refs(rule.head.ref) 20 | 21 | not util.is_snake_case(part.value) 22 | 23 | violation := result.fail(rego.metadata.chain(), result.location(part)) 24 | } 25 | 26 | report contains violation if { 27 | var := ast.found.vars[_][_][_] 28 | 29 | not util.is_snake_case(var.value) 30 | 31 | violation := result.fail(rego.metadata.chain(), result.location(var)) 32 | } 33 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/rule-length/rule_length.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Max rule length exceeded 3 | package regal.rules.style["rule-length"] 4 | 5 | import data.regal.config 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | cfg := config.rules.style["rule-length"] 11 | 12 | some rule in input.rules 13 | 14 | text := util.to_location_object(rule.location).text 15 | 16 | _line_count(cfg, text) > cfg[_max_length_property(rule.head.ref[0].value)] 17 | 18 | not _no_body_exception(cfg, rule) 19 | 20 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 21 | } 22 | 23 | _no_body_exception(cfg, rule) if { 24 | cfg["except-empty-body"] == true 25 | not rule.body 26 | } 27 | 28 | default _max_length_property(_) := "max-rule-length" 29 | 30 | _max_length_property(value) := "max-test-rule-length" if startswith(value, "test_") 31 | 32 | _line_count(cfg, text) := strings.count(text, "\n") + 1 if cfg["count-comments"] == true 33 | 34 | _line_count(cfg, text) := n if { 35 | not cfg["count-comments"] 36 | 37 | n := count([1 | 38 | some line in split(text, "\n") 39 | not startswith(trim_space(line), "#") 40 | ]) 41 | } 42 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/todo-comment/todo_comment.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Avoid TODO comments 3 | package regal.rules.style["todo-comment"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | todo_identifiers := ["todo", "TODO", "fixme", "FIXME"] 10 | todo_pattern := sprintf(`^\s*(%s)`, [concat("|", todo_identifiers)]) 11 | 12 | some comment in ast.comments_decoded 13 | regex.match(todo_pattern, comment.text) 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(comment)) 16 | } 17 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/trailing-default-rule/trailing_default_rule.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Default rule should be declared first 3 | package regal.rules.style["trailing-default-rule"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some i, rule in input.rules 10 | 11 | rule["default"] == true 12 | 13 | name := ast.ref_to_string(rule.head.ref) 14 | name in _all_names(array.slice(input.rules, 0, i)) 15 | 16 | violation := result.fail(rego.metadata.chain(), result.location(rule)) 17 | } 18 | 19 | _all_names(rules) := {name | 20 | some rule in rules 21 | name := ast.ref_to_string(rule.head.ref) 22 | } 23 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/trailing-default-rule/trailing_default_rule_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.style["trailing-default-rule_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.style["trailing-default-rule"] as rule 7 | 8 | test_success_default_declared_first if { 9 | module := ast.policy(` 10 | default foo := true 11 | 12 | foo if true 13 | `) 14 | r := rule.report with input as module 15 | 16 | r == set() 17 | } 18 | 19 | test_fail_default_declared_after if { 20 | module := ast.policy(` 21 | foo if true 22 | 23 | default foo := true 24 | `) 25 | r := rule.report with input as module 26 | 27 | r == {{ 28 | "category": "style", 29 | "description": "Default rule should be declared first", 30 | "level": "error", 31 | "location": { 32 | "col": 2, 33 | "file": "policy.rego", 34 | "row": 6, 35 | "end": { 36 | "col": 9, 37 | "row": 6, 38 | }, 39 | "text": "\tdefault foo := true", 40 | }, 41 | "related_resources": [{ 42 | "description": "documentation", 43 | "ref": config.docs.resolve_url("$baseUrl/$category/trailing-default-rule", "style"), 44 | }], 45 | "title": "trailing-default-rule", 46 | }} 47 | } 48 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/unnecessary-some/unnecessary_some.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Unnecessary use of `some` 3 | package regal.rules.style["unnecessary-some"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | # No need to traverse rules here if we're not importing `in` 10 | ast.imports_keyword(ast.imports, "in") 11 | 12 | symbols := ast.found.symbols[_][_] 13 | 14 | symbols[0].type == "call" 15 | symbols[0].value[0].type == "ref" 16 | 17 | _some_is_unnecessary(symbols[0].value, ast.scalar_types) 18 | 19 | violation := result.fail(rego.metadata.chain(), result.location(symbols)) 20 | } 21 | 22 | _some_is_unnecessary(symbol, scalar_types) if { 23 | symbol[0].value[0].value == "internal" 24 | symbol[0].value[1].value == "member_2" 25 | symbol[1].type in scalar_types 26 | } 27 | 28 | _some_is_unnecessary(symbol, scalar_types) if { 29 | symbol[0].value[0].value == "internal" 30 | symbol[0].value[1].value == "member_3" 31 | symbol[1].type in scalar_types 32 | symbol[2].type in scalar_types 33 | } 34 | -------------------------------------------------------------------------------- /bundle/regal/rules/style/yoda-condition/yoda_condition.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Yoda condition 3 | package regal.rules.style["yoda-condition"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | value := ast.found.calls[_][_] 10 | 11 | value[0].value[0].type == "var" 12 | value[0].value[0].value in {"equal", "neq", "gt", "lt", "gte", "lte"} 13 | value[1].type in ast.scalar_types 14 | 15 | not value[2].type in ast.scalar_types 16 | not _ref_with_vars(value[2].value) 17 | 18 | violation := result.fail(rego.metadata.chain(), result.infix_expr_location(value)) 19 | } 20 | 21 | _ref_with_vars(ref) if { 22 | count(ref) > 2 23 | some i, part in ref 24 | i > 0 25 | part.type == "var" 26 | } 27 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/dubious-print-sprintf/dubious_print_sprintf.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Dubious use of print and sprintf 3 | package regal.rules.testing["dubious-print-sprintf"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | # skip traversing refs if no print calls are registered 10 | "print" in ast.builtin_functions_called 11 | 12 | value := ast.found.calls[_][_] 13 | 14 | value[0].value[0].type == "var" 15 | value[0].value[0].value == "print" 16 | value[1].type == "call" 17 | value[1].value[0].type == "ref" 18 | value[1].value[0].value[0].type == "var" 19 | value[1].value[0].value[0].value == "sprintf" 20 | 21 | violation := result.fail(rego.metadata.chain(), result.location(value[1].value[0].value[0])) 22 | } 23 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/file-missing-test-suffix/file_missing_test_suffix.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Files containing tests should have a _test.rego suffix 3 | package regal.rules.testing["file-missing-test-suffix"] 4 | 5 | import data.regal.ast 6 | import data.regal.config 7 | import data.regal.result 8 | 9 | # METADATA 10 | # description: disabled when filename is unknown 11 | # custom: 12 | # severity: warn 13 | notices contains result.notice(rego.metadata.chain()) if "no_filename" in config.capabilities.special 14 | 15 | report contains violation if { 16 | count(ast.tests) > 0 17 | 18 | not _valid_test_file_name(input.regal.file.name) 19 | 20 | violation := result.fail(rego.metadata.chain(), {"location": {"file": input.regal.file.name}}) 21 | } 22 | 23 | _valid_test_file_name(filename) if endswith(filename, "_test.rego") 24 | _valid_test_file_name("test.rego") # Styra DAS convention considered OK 25 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/file-missing-test-suffix/file_missing_test_suffix_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.testing["file-missing-test-suffix_test"] 2 | 3 | import data.regal.config 4 | 5 | import data.regal.rules.testing["file-missing-test-suffix"] as rule 6 | 7 | test_fail_test_in_file_without_test_suffix if { 8 | ast := regal.parse_module("policy.rego", `package foo_test 9 | 10 | test_foo if { false } 11 | `) 12 | 13 | r := rule.report with input as ast 14 | r == {{ 15 | "category": "testing", 16 | "description": "Files containing tests should have a _test.rego suffix", 17 | "related_resources": [{ 18 | "description": "documentation", 19 | "ref": config.docs.resolve_url("$baseUrl/$category/file-missing-test-suffix", "testing"), 20 | }], 21 | "title": "file-missing-test-suffix", 22 | "location": {"file": "policy.rego"}, 23 | "level": "error", 24 | }} 25 | } 26 | 27 | test_success_test_in_file_with_test_suffix if { 28 | ast := regal.parse_module("policy_test.rego", `package policy_test 29 | 30 | test_foo if { false } 31 | `) 32 | 33 | r := rule.report with input as ast 34 | r == set() 35 | } 36 | 37 | test_success_test_in_file_named_test if { 38 | ast := regal.parse_module("test.rego", `package test 39 | 40 | test_foo if { false } 41 | `) 42 | 43 | r := rule.report with input as ast 44 | r == set() 45 | } 46 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/identically-named-tests/identically_named_tests.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Multiple tests with same name 3 | package regal.rules.testing["identically-named-tests"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | test_names := [ast.ref_to_string(rule.head.ref) | some rule in ast.tests] 10 | 11 | some i, name in test_names 12 | 13 | name in array.slice(test_names, 0, i) 14 | 15 | violation := result.fail(rego.metadata.chain(), result.location(_rule_by_name(name, ast.tests).head)) 16 | } 17 | 18 | _rule_by_name(name, rules) := regal.last([rule | 19 | some rule in rules 20 | rule.head.ref[0].value == name 21 | ]) 22 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/identically-named-tests/identically_named_tests_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.testing["identically-named-tests_test"] 2 | 3 | import data.regal.config 4 | import data.regal.rules.testing["identically-named-tests"] as rule 5 | 6 | test_fail_identically_named_tests if { 7 | ast := regal.parse_module("foo_test.rego", ` 8 | package foo_test 9 | 10 | test_foo if { false } 11 | test_foo if { true } 12 | `) 13 | r := rule.report with input as ast 14 | 15 | r == {{ 16 | "category": "testing", 17 | "description": "Multiple tests with same name", 18 | "related_resources": [{ 19 | "description": "documentation", 20 | "ref": config.docs.resolve_url("$baseUrl/$category/identically-named-tests", "testing"), 21 | }], 22 | "title": "identically-named-tests", 23 | "location": { 24 | "row": 5, 25 | "col": 2, 26 | "end": { 27 | "col": 10, 28 | "row": 5, 29 | }, 30 | "file": "foo_test.rego", 31 | "text": "\ttest_foo if { true }", 32 | }, 33 | "level": "error", 34 | }} 35 | } 36 | 37 | test_success_differently_named_tests if { 38 | ast := regal.parse_module("foo_test.rego", ` 39 | package foo_test 40 | 41 | test_foo if { false } 42 | test_bar if { true } 43 | test_baz if { 1 == 1 } 44 | `) 45 | r := rule.report with input as ast 46 | r == set() 47 | } 48 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/metasyntactic-variable/metasyntactic_variable.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Metasyntactic variable name 3 | package regal.rules.testing["metasyntactic-variable"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | _metasyntactic := { 9 | "foobar", 10 | "foo", 11 | "bar", 12 | "baz", 13 | "qux", 14 | "quux", 15 | "corge", 16 | "grault", 17 | "garply", 18 | "waldo", 19 | "fred", 20 | "plugh", 21 | "xyzzy", 22 | "thud", 23 | } 24 | 25 | report contains violation if { 26 | some rule in input.rules 27 | some part in ast.named_refs(rule.head.ref) 28 | 29 | lower(part.value) in _metasyntactic 30 | 31 | # In case we have chained rule bodies — only flag the location where we have an actual name: 32 | # foo { 33 | # input.x 34 | # } { 35 | # input.y 36 | # } 37 | not ast.is_chained_rule_body(rule, input.regal.file.lines) 38 | 39 | violation := result.fail(rego.metadata.chain(), result.location(part)) 40 | } 41 | 42 | report contains violation if { 43 | some i 44 | var := ast.found.vars[i][_][_] 45 | 46 | lower(var.value) in _metasyntactic 47 | 48 | ast.is_output_var(input.rules[to_number(i)], var) 49 | 50 | violation := result.fail(rego.metadata.chain(), result.location(var)) 51 | } 52 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/print-or-trace-call/print_or_trace_call.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Call to print or trace function 3 | package regal.rules.testing["print-or-trace-call"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | import data.regal.util 8 | 9 | report contains violation if { 10 | # skip iteration of refs if no print or trace calls are registered 11 | util.intersects(ast.builtin_functions_called, {"print", "trace"}) 12 | 13 | ref := ast.found.calls[_][_][0] 14 | 15 | ref.value[0].type == "var" 16 | ref.value[0].value in {"print", "trace"} 17 | 18 | violation := result.fail(rego.metadata.chain(), result.location(ref)) 19 | } 20 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/print-or-trace-call/print_or_trace_call_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.testing["print-or-trace-call_test"] 2 | 3 | import data.regal.ast 4 | import data.regal.capabilities 5 | import data.regal.config 6 | 7 | import data.regal.rules.testing["print-or-trace-call"] as rule 8 | 9 | test_fail_call_to_print_and_trace if { 10 | r := rule.report with input as ast.policy(`allow if { 11 | print("foo") 12 | 13 | x := [i | i = 0; trace("bar")] 14 | }`) 15 | with config.capabilities as capabilities.provided 16 | 17 | r == { 18 | expected_with_location({ 19 | "col": 3, 20 | "file": "policy.rego", 21 | "row": 4, 22 | "end": {"col": 8, "row": 4}, 23 | "text": "\t\tprint(\"foo\")", 24 | }), 25 | expected_with_location({ 26 | "col": 20, 27 | "file": "policy.rego", 28 | "row": 6, 29 | "end": {"col": 25, "row": 6}, 30 | "text": "\t\tx := [i | i = 0; trace(\"bar\")]", 31 | }), 32 | } 33 | } 34 | 35 | expected_with_location(location) := { 36 | "category": "testing", 37 | "description": "Call to print or trace function", 38 | "level": "error", 39 | "location": location, 40 | "related_resources": [{ 41 | "description": "documentation", 42 | "ref": config.docs.resolve_url("$baseUrl/$category/print-or-trace-call", "testing"), 43 | }], 44 | "title": "print-or-trace-call", 45 | } 46 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/test-outside-test-package/test_outside_test_package.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Test outside of test package 3 | package regal.rules.testing["test-outside-test-package"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | not _is_test_package(ast.package_name) 10 | 11 | some rule in ast.tests 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 14 | } 15 | 16 | _is_test_package(package_name) if endswith(package_name, "_test") 17 | _is_test_package("test") # Styra DAS convention considered OK 18 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/todo-test/todo_test.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: TODO test encountered 3 | package regal.rules.testing["todo-test"] 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | some rule in ast.rules 10 | 11 | startswith(ast.ref_to_string(rule.head.ref), "todo_test_") 12 | 13 | violation := result.fail(rego.metadata.chain(), result.location(rule.head)) 14 | } 15 | -------------------------------------------------------------------------------- /bundle/regal/rules/testing/todo-test/todo_test_test.rego: -------------------------------------------------------------------------------- 1 | package regal.rules.testing["todo-test_test"] 2 | 3 | import data.regal.config 4 | import data.regal.rules.testing["todo-test"] as rule 5 | 6 | test_fail_todo_test if { 7 | ast := regal.parse_module("foo_test.rego", ` 8 | package foo_test 9 | 10 | todo_test_foo if { false } 11 | 12 | test_bar if { true } 13 | `) 14 | r := rule.report with input as ast 15 | 16 | r == {{ 17 | "category": "testing", 18 | "description": "TODO test encountered", 19 | "related_resources": [{ 20 | "description": "documentation", 21 | "ref": config.docs.resolve_url("$baseUrl/$category/todo-test", "testing"), 22 | }], 23 | "title": "todo-test", 24 | "location": { 25 | "col": 2, 26 | "file": "foo_test.rego", 27 | "row": 4, 28 | "end": { 29 | "col": 15, 30 | "row": 4, 31 | }, 32 | "text": "\ttodo_test_foo if { false }", 33 | }, 34 | "level": "error", 35 | }} 36 | } 37 | -------------------------------------------------------------------------------- /cmd/capabilities.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/styrainc/regal/internal/compile" 11 | ) 12 | 13 | func init() { 14 | capabilitiesCommand := &cobra.Command{ 15 | Hidden: true, 16 | Use: "capabilities", 17 | Short: "Print the capabilities of Regal", 18 | Long: "Show capabilities for Regal", 19 | RunE: func(*cobra.Command, []string) error { 20 | bs, err := json.MarshalIndent(compile.Capabilities(), "", " ") 21 | if err != nil { 22 | return fmt.Errorf("failed marshalling capabilities: %w", err) 23 | } 24 | 25 | fmt.Fprintln(os.Stdout, string(bs)) 26 | 27 | return nil 28 | }, 29 | } 30 | 31 | RootCommand.AddCommand(capabilitiesCommand) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/constants.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const ( 4 | // formatJSON is the JSON format value for the --format flag in various commands. 5 | formatJSON = "json" 6 | // formatPretty is the pretty format value for the --format flag in various commands. 7 | formatPretty = "pretty" 8 | // formatCompact is the compact format value for the --format flag in various commands. 9 | formatCompact = "compact" 10 | // formatGitHub is the GitHub format value for the --format flag in various commands. 11 | formatGitHub = "github" 12 | // formatFestive is the festive format value for the --format flag in various commands. 13 | formatFestive = "festive" 14 | // formatSarif is the SARIF format value for the --format flag in various commands. 15 | formatSarif = "sarif" 16 | // formatJunit is the JUnit format value for the --format flag in various commands. 17 | formatJunit = "junit" 18 | ) 19 | -------------------------------------------------------------------------------- /cmd/exit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "fmt" 4 | 5 | type ExitError struct { 6 | code int 7 | } 8 | 9 | func (e ExitError) Error() string { 10 | return fmt.Sprintf("exit code %d", e.code) 11 | } 12 | 13 | func (e ExitError) Code() int { 14 | return e.code 15 | } 16 | 17 | func exit(code int) error { 18 | return ExitError{code: code} 19 | } 20 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // RootCommand is the base CLI command that all subcommands are added to. 11 | var RootCommand = &cobra.Command{ 12 | Use: path.Base(os.Args[0]), 13 | Short: "Regal", 14 | Long: "Regal is a linter for Rego, with the goal of making your Rego magnificent!", 15 | } 16 | -------------------------------------------------------------------------------- /docs/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | default: true 2 | 3 | # MD013/line-length 4 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md 5 | MD013: 6 | line_length: 120 7 | heading_line_length: 80 8 | code_blocks: false 9 | tables: false 10 | headings: true 11 | 12 | # MD024/no-duplicate-heading 13 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md 14 | MD024: 15 | siblings_only: true 16 | 17 | # MD026/no-trailing-punctuation 18 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md 19 | MD026: 20 | # Punctuation characters 21 | punctuation: ".,;:,;:!" 22 | 23 | # MD031/blanks-around-fences 24 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md 25 | MD031: false 26 | 27 | # MD033/no-inline-html 28 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md 29 | MD033: 30 | allowed_elements: 31 | - br 32 | - details 33 | - img 34 | - strong 35 | - summary 36 | 37 | # MD036/no-emphasis-as-heading 38 | # https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md 39 | MD036: false 40 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the most recent release of Regal is supported in the sense that it has any chance of seeing patch releases. 6 | 7 | We frequently do patch releases even for ordinary issues when found following a release, and security issues would 8 | be no exception. 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Security issues may be reported either by email to [devrel@styra.com](mailto:devrel@styra.com), or to the admins in the 13 | [Styra Community Slack](https://inviter.co/styra). You should expect a response within 24 14 | hours, but most likely much sooner. 15 | -------------------------------------------------------------------------------- /docs/adopters.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 13 2 | -------------------------------------------------------------------------------- /docs/architecture.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 4 2 | -------------------------------------------------------------------------------- /docs/assets/dap/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/dap/animation.gif -------------------------------------------------------------------------------- /docs/assets/dap/breakpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/dap/breakpoint.png -------------------------------------------------------------------------------- /docs/assets/dap/codeaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/dap/codeaction.png -------------------------------------------------------------------------------- /docs/assets/dap/print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/dap/print.png -------------------------------------------------------------------------------- /docs/assets/dap/variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/dap/variables.png -------------------------------------------------------------------------------- /docs/assets/editors-neovim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/editors-neovim.png -------------------------------------------------------------------------------- /docs/assets/evalcustom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/evalcustom.png -------------------------------------------------------------------------------- /docs/assets/lsp/code_action_fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/code_action_fix.png -------------------------------------------------------------------------------- /docs/assets/lsp/code_action_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/code_action_show.png -------------------------------------------------------------------------------- /docs/assets/lsp/codeaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/codeaction.png -------------------------------------------------------------------------------- /docs/assets/lsp/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/completion.png -------------------------------------------------------------------------------- /docs/assets/lsp/diagnostics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/diagnostics.png -------------------------------------------------------------------------------- /docs/assets/lsp/documentsymbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/documentsymbols.png -------------------------------------------------------------------------------- /docs/assets/lsp/documentsymbols2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/documentsymbols2.png -------------------------------------------------------------------------------- /docs/assets/lsp/eval_use_as_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/eval_use_as_input.png -------------------------------------------------------------------------------- /docs/assets/lsp/evalcodelens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/evalcodelens.png -------------------------------------------------------------------------------- /docs/assets/lsp/evalcodelensprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/evalcodelensprint.png -------------------------------------------------------------------------------- /docs/assets/lsp/folding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/folding.png -------------------------------------------------------------------------------- /docs/assets/lsp/format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/format.png -------------------------------------------------------------------------------- /docs/assets/lsp/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/hover.png -------------------------------------------------------------------------------- /docs/assets/lsp/inlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/lsp/inlay.png -------------------------------------------------------------------------------- /docs/assets/regal-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/regal-banner.png -------------------------------------------------------------------------------- /docs/assets/regal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/regal.jpg -------------------------------------------------------------------------------- /docs/assets/regal_cncf_london.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/regal_cncf_london.png -------------------------------------------------------------------------------- /docs/assets/rules/pkg_name_completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyraInc/regal/72fb730e72a139504b5b555cfb3a608815b4e374/docs/assets/rules/pkg_name_completion.png -------------------------------------------------------------------------------- /docs/cicd.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_label: CI/CD 2 | sidebar_position: 8 3 | -------------------------------------------------------------------------------- /docs/custom-rules.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 5 2 | -------------------------------------------------------------------------------- /docs/debug-adapter.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 10 2 | -------------------------------------------------------------------------------- /docs/editor-support.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 7 2 | -------------------------------------------------------------------------------- /docs/fixing.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 3 2 | -------------------------------------------------------------------------------- /docs/integration.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 12 2 | sidebar_label: Go Integration 3 | -------------------------------------------------------------------------------- /docs/language-server.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 9 2 | -------------------------------------------------------------------------------- /docs/opa-one-dot-zero.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_label: OPA 1.0 2 | sidebar_position: 14 3 | -------------------------------------------------------------------------------- /docs/pre-commit-hooks.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 6 2 | -------------------------------------------------------------------------------- /docs/readme-sections/badges.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Build Status](https://github.com/styrainc/regal/workflows/Build/badge.svg)](https://github.com/styrainc/regal/actions) 4 | ![OPA v1.5.0](https://openpolicyagent.org/badge/v1.5.0) 5 | [![codecov](https://codecov.io/github/StyraInc/regal/graph/badge.svg?token=EQK01YF3X3)](https://codecov.io/github/StyraInc/regal) 6 | [![Downloads](https://img.shields.io/github/downloads/styrainc/regal/total.svg)](https://github.com/StyraInc/regal/releases) 7 | -------------------------------------------------------------------------------- /docs/readme-sections/exit.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Exit Codes 4 | 5 | Exit codes are used to indicate the result of the `lint` command. The `--fail-level` provided for `regal lint` may be 6 | used to change the exit code behavior, and allows a value of either `warning` or `error` (default). 7 | 8 | If `--fail-level error` is supplied, exit code will be zero even if warnings are present: 9 | 10 | - `0`: no errors were found 11 | - `0`: one or more warnings were found 12 | - `3`: one or more errors were found 13 | 14 | This is the default behavior. 15 | 16 | If `--fail-level warning` is supplied, warnings will result in a non-zero exit code: 17 | 18 | - `0`: no errors or warnings were found 19 | - `2`: one or more warnings were found 20 | - `3`: one or more errors were found 21 | -------------------------------------------------------------------------------- /docs/readme-sections/footer.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Community 4 | 5 | For questions, discussions and announcements related to Styra products, services and open source projects, please join 6 | the Styra community on [Slack](https://inviter.co/styra)! 7 | -------------------------------------------------------------------------------- /docs/readme-sections/github-manifest: -------------------------------------------------------------------------------- 1 | title.md 2 | badges.md 3 | intro.md 4 | image-definition.md 5 | goals.md 6 | testimonials.md 7 | getting-started.md 8 | rules.md 9 | configuration.md 10 | ignore-rules.md 11 | rego-version.md 12 | project-roots.md 13 | capabilities.md 14 | exit.md 15 | output.md 16 | strict.md 17 | ls.md 18 | opa1.md 19 | resources.md 20 | status.md 21 | roadmap.md 22 | footer.md 23 | -------------------------------------------------------------------------------- /docs/readme-sections/goals.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Goals 4 | 5 | - Deliver an outstanding policy development experience by providing the best possible tools for that purpose 6 | - Identify common mistakes, bugs and inefficiencies in Rego policies, and suggest better approaches 7 | - Provide advice on [best practices](https://github.com/StyraInc/rego-style-guide), coding style, and tooling 8 | - Allow users, teams and organizations to enforce custom rules on their policy code 9 | -------------------------------------------------------------------------------- /docs/readme-sections/image-definition.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | illustration of a viking representing the Regal logo 8 | 9 | > regal 10 | > 11 | > adj : of notable excellence or magnificence : splendid 12 | 13 | \- [Merriam Webster](https://www.merriam-webster.com/dictionary/regal) 14 | -------------------------------------------------------------------------------- /docs/readme-sections/intro.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Regal is a linter and language server for [Rego](https://www.openpolicyagent.org/docs/policy-language/), making 4 | your Rego magnificent, and you the ruler of rules! 5 | 6 | With its extensive set of linter rules, documentation and editor integrations, Regal is the perfect companion for policy 7 | development, whether you're an experienced Rego developer or just starting out. 8 | -------------------------------------------------------------------------------- /docs/readme-sections/opa1.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Regal and OPA 1.0+ 4 | 5 | Starting from version v0.30.0, Regal supports working with both 6 | [OPA 1.0+]((https://blog.openpolicyagent.org/announcing-opa-1-0-a-new-standard-for-policy-as-code-a6d8427ee828)) 7 | policies and Rego from earlier versions of OPA. While everything should work without additional configuration, 8 | we recommend checking out our documentation on using Regal with [OPA 1.0](https://docs.styra.com/regal/opa-one-dot-zero) 9 | and later for the best possible experience managing projects of any given Rego version, or even a mix of them. 10 | -------------------------------------------------------------------------------- /docs/readme-sections/output.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Output Formats 4 | 5 | The `regal lint` command allows specifying the output format by using the `--format` flag. The available output formats 6 | are: 7 | 8 | - `pretty` (default) - Human-readable table-like output where each violation is printed with a detailed explanation 9 | - `compact` - Human-readable output where each violation is printed on a single line 10 | - `json` - JSON output, suitable for programmatic consumption 11 | - `github` - GitHub [workflow command](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions) 12 | output, ideal for use in GitHub Actions. Annotates PRs and creates a 13 | [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary) 14 | from the linter report 15 | - `sarif` - [SARIF](https://sarifweb.azurewebsites.net/) JSON output, for consumption by tools processing code analysis 16 | reports 17 | - `junit` - JUnit XML output, e.g. for CI servers like GitLab that show these results in a merge request. 18 | -------------------------------------------------------------------------------- /docs/readme-sections/roadmap-website.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Roadmap 4 | 5 | The current Roadmap items are all related to the preparation for 6 | [Regal 1.0](https://github.com/StyraInc/regal/issues/979): 7 | 8 | - [Go API: Refactor the Location object in Violation (#1554)](https://github.com/StyraInc/regal/issues/1554) 9 | - [Rego API: Provide a stable and well-documented Rego API (#1555)](https://github.com/StyraInc/regal/issues/1555) 10 | - [Go API: Audit and reduce the public Go API surface (#1556)](https://github.com/StyraInc/regal/issues/1556) 11 | - [Custom Rules: Tighten up Authoring experience (#1559)](https://github.com/StyraInc/regal/issues/1559) 12 | - [docs: Improve automated documentation generation (#1557)](https://github.com/StyraInc/regal/issues/1557) 13 | - [docs: Break down README into smaller units (#1558)](https://github.com/StyraInc/regal/issues/1558) 14 | - [lsp: Support a JetBrains LSP client (#1560)](https://github.com/StyraInc/regal/issues/1560) 15 | 16 | If there's something you'd like to have added to the roadmap, either open an issue, or reach out in the community Slack! 17 | -------------------------------------------------------------------------------- /docs/readme-sections/roadmap.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Roadmap 4 | 5 | The current Roadmap items are all related to the preparation for 6 | [Regal 1.0](https://github.com/StyraInc/regal/issues/979): 7 | 8 | - [ ] [Go API: Refactor the Location object in Violation (#1554)](https://github.com/StyraInc/regal/issues/1554) 9 | - [ ] [Rego API: Provide a stable and well-documented Rego API (#1555)](https://github.com/StyraInc/regal/issues/1555) 10 | - [ ] [Go API: Audit and reduce the public Go API surface (#1556)](https://github.com/StyraInc/regal/issues/1556) 11 | - [ ] [Custom Rules: Tighten up Authoring experience (#1559)](https://github.com/StyraInc/regal/issues/1559) 12 | - [ ] [docs: Improve automated documentation generation (#1557)](https://github.com/StyraInc/regal/issues/1557) 13 | - [ ] [docs: Break down README into smaller units (#1558)](https://github.com/StyraInc/regal/issues/1558) 14 | - [ ] [lsp: Support a JetBrains LSP client (#1560)](https://github.com/StyraInc/regal/issues/1560) 15 | 16 | If there's something you'd like to have added to the roadmap, either open an issue, or reach out in the community Slack! 17 | -------------------------------------------------------------------------------- /docs/readme-sections/status.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Status 4 | 5 | Regal is currently in beta. End-users should not expect any drastic changes, but any API may change without notice. 6 | If you want to embed Regal in another project or product, please reach out! 7 | -------------------------------------------------------------------------------- /docs/readme-sections/strict.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## OPA Check and Strict Mode 4 | 5 | OPA itself provides a "linter" of sorts, via the `opa check` command and its `--strict` flag. This checks the provided 6 | Rego files not only for syntax errors, but also for OPA 7 | [strict mode](https://www.openpolicyagent.org/docs/policy-language/#strict-mode) violations. Most of the strict 8 | mode checks from before OPA 1.0 have now been made default checks in OPA, and only two additional checks are currently 9 | provided by the `--strict` flag. Those are both important checks not covered by Regal though, so our recommendation is 10 | to run `opa check --strict` against your policies before linting with Regal. 11 | -------------------------------------------------------------------------------- /docs/readme-sections/testimonials.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What People Say About Regal 4 | 5 | > I really like that at each release of Regal I learn something new! 6 | > Of all the linters I'm exposed to, Regal is probably the most instructive one. 7 | 8 | — Leonardo Taccari, [NetBSD](https://www.netbsd.org/) 9 | 10 | > Reviewing the Regal rules documentation. Pure gold. 11 | 12 | — Dima Korolev, [Miro](https://miro.com/) 13 | 14 | > Such an awesome project! 15 | 16 | — Shawn McGuire, [Atlassian](https://www.atlassian.com/) 17 | 18 | > I am really impressed with Regal. It has helped me write more expressive and deterministic Rego. 19 | 20 | — Jimmy Ray, [Boeing](https://www.boeing.com/) 21 | 22 | See the [adopters](https://docs.styra.com/regal/adopters) file for more Regal users. 23 | -------------------------------------------------------------------------------- /docs/readme-sections/title.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Regal 6 | -------------------------------------------------------------------------------- /docs/readme-sections/website-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | image: /img/regal.png 3 | sidebar_position: 1 4 | sidebar_label: Introduction 5 | --- 6 | 7 | 8 | 9 | import RegalIntro from '@site/src/components/RegalIntro'; 10 | 11 | # Regal 12 | 13 | Regal is a linter and language server for 14 | [Rego](https://www.openpolicyagent.org/docs/policy-language/), making 15 | your Rego magnificent, and you the ruler of rules! 16 | 17 | With its extensive set of linter rules, documentation and editor integrations, 18 | Regal is the perfect companion for policy development, whether you're an 19 | experienced Rego developer or just starting out. 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/readme-sections/website-manifest: -------------------------------------------------------------------------------- 1 | website-intro.md 2 | goals.md 3 | testimonials.md 4 | getting-started.md 5 | rules.md 6 | configuration.md 7 | ignore-rules.md 8 | rego-version.md 9 | project-roots.md 10 | capabilities.md 11 | exit.md 12 | output.md 13 | strict.md 14 | ls.md 15 | opa1.md 16 | resources-website.md 17 | status.md 18 | roadmap-website.md 19 | footer.md 20 | -------------------------------------------------------------------------------- /docs/remote-features.md: -------------------------------------------------------------------------------- 1 | # Remote Features 2 | 3 | This page outlines the features of Regal that need internet access to function. 4 | 5 | ## Checking for Updates 6 | 7 | Regal will check for updates on startup. If a new version is available, 8 | Regal will notify you by writing a message in stderr. 9 | 10 | An example of such a message is: 11 | 12 | ```txt 13 | A new version of Regal is available (v0.23.1). You are running v0.23.0. 14 | See https://github.com/StyraInc/regal/releases/tag/v0.23.1 for the latest release. 15 | ``` 16 | 17 | This message is based on the local version set in the Regal binary, and **no 18 | user data is sent** to GitHub where the releases are hosted. 19 | 20 | This same function will also write to the file at: `$HOME/.config/regal/latest_version.json`, 21 | this is used as a cache of the latest version to avoid consuming excessive 22 | GitHub API rate limits when using Regal. 23 | 24 | This functionality can be disabled in two ways: 25 | 26 | * Using `.regal/config.yaml` / `.regal.yaml`: set `features.remote.check-version` to `false`. 27 | * Using an environment variable: set `REGAL_DISABLE_CHECK_VERSION` to `true`. 28 | -------------------------------------------------------------------------------- /docs/remote-features.md.yaml: -------------------------------------------------------------------------------- 1 | sidebar_position: 11 2 | -------------------------------------------------------------------------------- /docs/rules/_category_.json: -------------------------------------------------------------------------------- 1 | { "collapsed": false } 2 | -------------------------------------------------------------------------------- /docs/rules/bugs/constant-condition.md: -------------------------------------------------------------------------------- 1 | # constant-condition 2 | 3 | **Summary**: Constant condition 4 | 5 | **Category**: Bugs 6 | 7 | **Avoid** 8 | ```rego 9 | package policy 10 | 11 | allow if { 12 | 1 == 1 13 | } 14 | ``` 15 | 16 | **Prefer** 17 | ```rego 18 | package policy 19 | 20 | allow := true 21 | ``` 22 | 23 | ## Rationale 24 | 25 | While most often a mistake, constant conditions are sometimes used as placeholders, or "TODO logic". While this is 26 | harmless, it has no place in production policy, and should be replaced or removed before deployment. 27 | 28 | ## Configuration Options 29 | 30 | This linter rule provides the following configuration options: 31 | 32 | ```yaml 33 | rules: 34 | bugs: 35 | constant-condition: 36 | # one of "error", "warning", "ignore" 37 | level: error 38 | ``` 39 | 40 | ## Related Resources 41 | 42 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/bugs/constant-condition/constant_condition.rego) 43 | 44 | ## Community 45 | 46 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 47 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 48 | [Slack](https://inviter.co/styra)! 49 | -------------------------------------------------------------------------------- /docs/rules/bugs/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Bugs", 3 | "sidebar_position": 1, 4 | "description": "Rules that detect bugs in your code." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/bugs/unused-return-value.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | --- 4 | 5 | # unused-return-value 6 | 7 | ## Please Note 8 | 9 | This rule has been renamed to *unassigned-return-value* and can be found 10 | [here](https://docs.styra.com/regal/rules/bugs/unassigned-return-value). 11 | -------------------------------------------------------------------------------- /docs/rules/custom/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Custom", 3 | "sidebar_position": 7, 4 | "description": "Configurable rules that can be customized to your needs." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/idiomatic/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Idiomatic", 3 | "sidebar_position": 2, 4 | "description": "Rules that enforce idiomatic code." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/imports/import-after-rule.md: -------------------------------------------------------------------------------- 1 | # import-after-rule 2 | 3 | **Summary**: Import declared after rule 4 | 5 | **Category**: Imports 6 | 7 | **Avoid** 8 | ```rego 9 | package policy 10 | 11 | required_role := "developer" 12 | 13 | import data.identity.users 14 | ``` 15 | 16 | **Prefer** 17 | ```rego 18 | package policy 19 | 20 | import data.identity.users 21 | 22 | required_role := "developer" 23 | ``` 24 | 25 | ## Rationale 26 | 27 | Imports should be declared at the top of a policy, and before any rules. This makes it easy to quickly see the 28 | dependencies imported in the policy simply by looking at the top of the file. 29 | 30 | ## Configuration Options 31 | 32 | This linter rule provides the following configuration options: 33 | 34 | ```yaml 35 | rules: 36 | imports: 37 | import-after-rule: 38 | # one of "error", "warning", "ignore" 39 | level: error 40 | ``` 41 | 42 | ## Related Resources 43 | 44 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/imports/import-after-rule/import_after_rule.rego) 45 | 46 | ## Community 47 | 48 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 49 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 50 | [Slack](https://inviter.co/styra)! 51 | -------------------------------------------------------------------------------- /docs/rules/imports/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Imports", 3 | "sidebar_position": 3, 4 | "description": "Rules related to importing of packages and keywords." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/imports/redundant-alias.md: -------------------------------------------------------------------------------- 1 | # redundant-alias 2 | 3 | **Summary**: Redundant alias 4 | 5 | **Category**: Imports 6 | 7 | **Avoid** 8 | ```rego 9 | package policy 10 | 11 | import data.users.permissions as permissions 12 | ``` 13 | 14 | **Prefer** 15 | ```rego 16 | package policy 17 | 18 | import data.users.permissions 19 | ``` 20 | 21 | ## Rationale 22 | 23 | The last component of an import path can always be referenced by the last 24 | component of the import path itself inside the package in which it's imported. 25 | Using an alias with the same name is thus redundant, and should be omitted. 26 | 27 | ## Configuration Options 28 | 29 | This linter rule provides the following configuration options: 30 | 31 | ```yaml 32 | rules: 33 | imports: 34 | redundant-alias: 35 | # one of "error", "warning", "ignore" 36 | level: error 37 | ``` 38 | 39 | ## Related Resources 40 | 41 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/imports/redundant-alias/redundant_alias.rego) 42 | 43 | ## Community 44 | 45 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 46 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 47 | [Slack](https://inviter.co/styra)! 48 | -------------------------------------------------------------------------------- /docs/rules/imports/redundant-data-import.md: -------------------------------------------------------------------------------- 1 | # redundant-data-import 2 | 3 | **Summary**: Redundant import of data 4 | 5 | **Category**: Imports 6 | 7 | **Avoid** 8 | ```rego 9 | package policy 10 | 11 | import data 12 | ``` 13 | 14 | ## Rationale 15 | 16 | Just like `input`, `data` is always globally available and does not need to be imported. 17 | 18 | ## Configuration Options 19 | 20 | This linter rule provides the following configuration options: 21 | 22 | ```yaml 23 | rules: 24 | imports: 25 | redundant-data-import: 26 | # one of "error", "warning", "ignore" 27 | level: error 28 | ``` 29 | 30 | ## Related Resources 31 | 32 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/imports/redundant-data-import/redundant_data_import.rego) 33 | 34 | ## Community 35 | 36 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 37 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 38 | [Slack](https://inviter.co/styra)! 39 | -------------------------------------------------------------------------------- /docs/rules/performance/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Performance", 3 | "sidebar_position": 4, 4 | "description": "Rules to help identify possible performance issues." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/style/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Style", 3 | "sidebar_position": 5, 4 | "description": "Rules relating to code style." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/style/use-in-operator.md: -------------------------------------------------------------------------------- 1 | # use-in-operator 2 | 3 | ## Please Note 4 | 5 | This rule has been moved to *idiomatic* category and can be found [here](../idiomatic/use-in-operator.md). 6 | -------------------------------------------------------------------------------- /docs/rules/testing/file-missing-test-suffix.md: -------------------------------------------------------------------------------- 1 | # file-missing-test-suffix 2 | 3 | **Summary**: Files containing tests should have a `_test.rego` suffix 4 | 5 | **Category**: Testing 6 | 7 | ## Rationale 8 | 9 | In order to clearly communicate intent, and to avoid bundling tests with production policy, tests should be kept in a 10 | separate file with a `_test.rego` suffix, and ideally prefixed with the same name as the policy the tests are targeting, 11 | e.g. `policy.rego` and `policy_test.rego`. 12 | 13 | ## Configuration Options 14 | 15 | This linter rule provides the following configuration options: 16 | 17 | ```yaml 18 | rules: 19 | testing: 20 | file-missing-test-suffix: 21 | # one of "error", "warning", "ignore" 22 | level: error 23 | ``` 24 | 25 | ## Related Resources 26 | 27 | - OPA Docs: [Policy Testing](https://www.openpolicyagent.org/docs/policy-testing/) 28 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/testing/file-missing-test-suffix/file_missing_test_suffix.rego) 29 | 30 | ## Community 31 | 32 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 33 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 34 | [Slack](https://inviter.co/styra)! 35 | -------------------------------------------------------------------------------- /docs/rules/testing/index.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Testing", 3 | "sidebar_position": 6, 4 | "description": "Rules relading to Rego tests." 5 | } 6 | -------------------------------------------------------------------------------- /docs/rules/testing/todo-test.md: -------------------------------------------------------------------------------- 1 | # todo-test 2 | 3 | **Summary**: TODO test encountered 4 | 5 | **Category**: Testing 6 | 7 | **Avoid** 8 | ```rego 9 | package policy_test 10 | 11 | import data.policy 12 | 13 | # Make sure this passes 14 | todo_test_allow_if_admin { 15 | policy.allow with input as {"user": {"roles": ["admin"]}} 16 | } 17 | ``` 18 | 19 | ## Rationale 20 | 21 | Writing TODO tests by prefixing `todo_` to any test is a good way to keep track of tests that need to be written while 22 | developing policy. They are however not to be committed, and should be removed before submitting the change for review. 23 | 24 | ## Configuration Options 25 | 26 | This linter rule provides the following configuration options: 27 | 28 | ```yaml 29 | rules: 30 | testing: 31 | todo-test: 32 | # one of "error", "warning", "ignore" 33 | level: error 34 | ``` 35 | 36 | ## Related Resources 37 | 38 | - OPA Docs: [Policy Testing](https://www.openpolicyagent.org/docs/policy-testing/) 39 | - GitHub: [Source Code](https://github.com/StyraInc/regal/blob/main/bundle/regal/rules/testing/todo-test/todo_test.rego) 40 | 41 | ## Community 42 | 43 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 44 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 45 | [Slack](https://inviter.co/styra)! 46 | -------------------------------------------------------------------------------- /e2e/e2e_conf.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | rego-version: 1 3 | roots: 4 | - path: v0 5 | rego-version: 0 6 | 7 | rules: 8 | style: 9 | external-reference: 10 | max-allowed: 0 11 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/custom/regal/rules/testcase/aggregates/custom_rules_using_aggregates.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Collect data in aggregates and validate it 3 | package custom.regal.rules.testcase["aggregates"] 4 | 5 | import data.regal.result 6 | 7 | aggregate contains result.aggregate(rego.metadata.chain(), {}) 8 | 9 | aggregate_report contains violation if { 10 | not two_files_processed 11 | 12 | violation := result.fail(rego.metadata.chain(), {}) 13 | } 14 | 15 | two_files_processed if count(input.aggregate) == 2 16 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/custom/regal/rules/testcase/empty_aggregate/empty_aggregate.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: | 3 | # Test to ensure a custom rule that aggregated no data is still reported 4 | # related_resources: 5 | # - description: issue 6 | # ref: https://github.com/StyraInc/regal/issues/1259 7 | package custom.regal.rules.testcase.empty_aggregates 8 | 9 | import data.regal.result 10 | 11 | aggregate contains result.aggregate(rego.metadata.chain(), {}) if { 12 | input.nope 13 | } 14 | 15 | aggregate_report contains violation if { 16 | count(input.aggregate) == 0 17 | 18 | violation := result.fail(rego.metadata.chain(), {}) 19 | } 20 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/ignore_directive/first.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: ignore_directive 3 | package ignore_directive 4 | 5 | import rego.v1 6 | 7 | # regal ignore:unresolved-import 8 | import data.unresolved 9 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/ignore_directive/second.rego: -------------------------------------------------------------------------------- 1 | package ignore_directive 2 | 3 | import rego.v1 4 | 5 | import data.unresolved 6 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/three_policies/policy_1.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: three policies 3 | package three_policies 4 | 5 | import rego.v1 6 | 7 | # METADATA 8 | # entrypoint: true 9 | my_policy_1 := true 10 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/three_policies/policy_2.rego: -------------------------------------------------------------------------------- 1 | package three_policies 2 | 3 | import rego.v1 4 | 5 | # METADATA 6 | # title: export 7 | export := [] 8 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/three_policies/policy_3.rego: -------------------------------------------------------------------------------- 1 | package three_policies 2 | 3 | import rego.v1 4 | 5 | # METADATA 6 | # title: my_policy_3 7 | my_policy_3 := true 8 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/two_policies/policy_1.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # title: two policies 3 | package two_policies 4 | 5 | import rego.v1 6 | 7 | # METADATA 8 | # entrypoint: true 9 | my_policy_1 := true 10 | -------------------------------------------------------------------------------- /e2e/testdata/aggregates/two_policies/policy_2.rego: -------------------------------------------------------------------------------- 1 | package two_policies 2 | 3 | import rego.v1 4 | 5 | # METADATA 6 | # title: export 7 | export := [] 8 | -------------------------------------------------------------------------------- /e2e/testdata/ast_type_failure/custom_type_fail.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Custom rule with type checker failure 3 | # schemas: 4 | # - input: schema.regal.ast 5 | package custom.regal.rules.naming.type_fail 6 | 7 | report contains foo if { 8 | # There is no "foo" attribute in the AST, 9 | # so this should fail at compile time 10 | foo := input.foo 11 | foo == "bar" 12 | } 13 | -------------------------------------------------------------------------------- /e2e/testdata/bugs/issue_1082.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: ensure that multiple custom fields are handled correctly 3 | # related_resources: 4 | # - description: issue 5 | # ref: https://github.com/StyraInc/regal/issues/1082 6 | # custom: 7 | # x: 1 8 | # y: 2 9 | package bug 10 | 11 | import rego.v1 12 | -------------------------------------------------------------------------------- /e2e/testdata/capabilities/custom_has_key.rego: -------------------------------------------------------------------------------- 1 | package capabilities 2 | 3 | import rego.v1 4 | 5 | # custom_has_key 6 | has_key(map, key) if { 7 | _ = map[key] 8 | } 9 | -------------------------------------------------------------------------------- /e2e/testdata/capabilities/custom_has_key_2.rego: -------------------------------------------------------------------------------- 1 | package capabilities 2 | 3 | import rego.v1 4 | 5 | # This is here to make sure we deal with multiple notices correctly, 6 | # and don't report duplicates multiple times. 7 | has_key(map, key) if { 8 | _ = map[key] 9 | } 10 | -------------------------------------------------------------------------------- /e2e/testdata/configs/custom_naming_convention.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | custom: 3 | naming-convention: 4 | level: error 5 | conventions: 6 | - pattern: "^_[a-z_]+$|^allow$" 7 | targets: 8 | - rule 9 | - pattern: '^acmecorp\.[a-z_\.]+$' 10 | targets: 11 | - package 12 | -------------------------------------------------------------------------------- /e2e/testdata/configs/defaulting.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | default: 3 | level: ignore 4 | bugs: 5 | default: 6 | level: error 7 | rule-shadows-builtin: 8 | level: ignore 9 | testing: 10 | print-or-trace-call: 11 | level: error 12 | -------------------------------------------------------------------------------- /e2e/testdata/configs/ignore_files_prefer_snake_case.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | style: 3 | file-length: 4 | level: ignore 5 | prefer-snake-case: 6 | level: error 7 | ignore: 8 | files: 9 | - "*.rego" 10 | -------------------------------------------------------------------------------- /e2e/testdata/configs/opa_v46_capabilities.yaml: -------------------------------------------------------------------------------- 1 | capabilities: 2 | from: 3 | engine: opa 4 | # object.keys was included in v0.47.0, 5 | # so we're using this to test that the 6 | # custom-has-key-construct rule won't 7 | # run given this version 8 | version: v0.46.0 9 | rules: 10 | idiomatic: 11 | no-defined-entrypoint: 12 | level: ignore 13 | custom-has-key-construct: 14 | level: error 15 | -------------------------------------------------------------------------------- /e2e/testdata/configs/rule_without_level.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | style: 3 | file-length: 4 | max-file-length: 1 5 | -------------------------------------------------------------------------------- /e2e/testdata/configs/v0-with-import-rego-v1.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | rego-version: 0 3 | capabilities: 4 | from: 5 | engine: opa 6 | version: v0.59.0 7 | -------------------------------------------------------------------------------- /e2e/testdata/configs/v0.yaml: -------------------------------------------------------------------------------- 1 | capabilities: 2 | from: 3 | engine: opa 4 | version: v0.58.0 5 | -------------------------------------------------------------------------------- /e2e/testdata/custom_naming_convention/policy.rego: -------------------------------------------------------------------------------- 1 | # package name must start with "acmecorp" 2 | package custom_naming_convention 3 | 4 | import rego.v1 5 | 6 | # rules must either start with "_" or be named "allow" 7 | naming_convention_fail if { 8 | input.foo == "bar" 9 | } 10 | 11 | _this_is_ok := true 12 | 13 | allow := true 14 | -------------------------------------------------------------------------------- /e2e/testdata/custom_rules/custom.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Custom rule description 3 | package custom.regal.rules.naming.myrule 4 | 5 | import rego.v1 6 | 7 | import data.regal.result 8 | 9 | report contains result.fail(rego.metadata.chain(), result.location(input["package"].path[1])) 10 | -------------------------------------------------------------------------------- /e2e/testdata/defaulting/example.rego: -------------------------------------------------------------------------------- 1 | package p 2 | import rego.v1 3 | boo := input.hoo[_] 4 | opa_fmt := "fail" 5 | or := 1 6 | 7 | allow if { 8 | print("hello") 9 | } 10 | -------------------------------------------------------------------------------- /e2e/testdata/v0/not_rego_v1.rego: -------------------------------------------------------------------------------- 1 | package legacy 2 | 3 | # implicit-future-keywords 4 | import future.keywords 5 | 6 | use_if { 7 | data.foo 8 | } 9 | 10 | use_contains[item] { 11 | some item in input.items 12 | } 13 | -------------------------------------------------------------------------------- /e2e/testdata/v0/rule_named_if.rego: -------------------------------------------------------------------------------- 1 | package rule_named_if 2 | 3 | allow := true if { 4 | input.foo == "bar" 5 | } 6 | -------------------------------------------------------------------------------- /e2e/testdata/violations/circular_import.rego: -------------------------------------------------------------------------------- 1 | package circular_import 2 | 3 | import data.all_violations 4 | 5 | x := 2 6 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | ) 8 | 9 | // 2 allocs. 10 | func BenchmarkPut(b *testing.B) { 11 | cache := NewBaseCache() 12 | ref := ast.MustParseRef("data.foo.bar.baz") 13 | value := ast.String("qux") 14 | 15 | for range b.N { 16 | cache.Put(ref, value) 17 | } 18 | } 19 | 20 | // 0 allocs. 21 | func BenchmarkGet(b *testing.B) { 22 | cache := NewBaseCache() 23 | ref := ast.MustParseRef("data.foo.bar") 24 | value := ast.NewObject(ast.Item(ast.StringTerm("baz"), ast.StringTerm("qux"))) 25 | cache.Put(ref, value) 26 | 27 | r := ast.MustParseRef("data.foo.bar.baz") 28 | 29 | for range b.N { 30 | _ = cache.Get(r) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/capabilities/capabilities_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package capabilities 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | ) 10 | 11 | // Since this test requires internet access, we hide it behind a flag. 12 | 13 | func TestLookupFromURL(t *testing.T) { 14 | t.Parallel() 15 | 16 | // Test that we can load a one of the existing OPA capabilities files 17 | // via GitHub. 18 | 19 | caps, err := Lookup( 20 | context.Background(), 21 | "https://raw.githubusercontent.com/open-policy-agent/opa/main/capabilities/v0.55.0.json", 22 | ) 23 | if err != nil { 24 | t.Errorf("unexpected error from Lookup: %v", err) 25 | } 26 | 27 | if len(caps.Builtins) != 193 { 28 | t.Errorf("OPA v0.55.0 capabilities should have 193 builtins, not %d", len(caps.Builtins)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/capabilities/embedded/embedded_test.go: -------------------------------------------------------------------------------- 1 | package embedded 2 | 3 | import "testing" 4 | 5 | func TestEmbeddedEOPA(t *testing.T) { 6 | t.Parallel() 7 | 8 | // As of 2024-08-27, there are 47 capabilities files in the EOPA repo. 9 | // It follows that there should never be less than 47 valid 10 | // capabilities in the embedded database. This is really just a sanity 11 | // check to ensure the JSON files didn't get misplaced or something to 12 | // that effect. 13 | // 14 | // This also ensures that all of the embedded capabilities files are 15 | // valid JSON we can successfully marshal into *ast.Capabilities. 16 | 17 | versions, err := LoadCapabilitiesVersions("eopa") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | if len(versions) < 47 { 23 | t.Errorf("Expected at least 47 EOPA capabilities in the embedded database (got %d)", len(versions)) 24 | } 25 | 26 | for _, v := range versions { 27 | caps, err := LoadCapabilitiesVersion("eopa", v) 28 | if err != nil { 29 | t.Errorf("error with eopa capabilities version %s: %v", v, err) 30 | } 31 | 32 | if len(caps.Builtins) < 1 { 33 | t.Errorf("eopa capabilities version %s has no builtins", v) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/capabilities/testdata/capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "unittest123", 5 | "description": "dummy used to test loading capabilities", 6 | "categories": [ 7 | "numbers" 8 | ], 9 | "decl": { 10 | "args": [ 11 | { 12 | "name": "x", 13 | "type": "number" 14 | } 15 | ], 16 | "result": { 17 | "description": "the absolute value of `x`", 18 | "name": "y", 19 | "type": "number" 20 | }, 21 | "type": "function" 22 | } 23 | } 24 | ], 25 | "future_keywords": [], 26 | "features": [] 27 | } 28 | -------------------------------------------------------------------------------- /internal/docs/docs.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | const docsBaseURL = "https://docs.styra.com/regal/rules" 4 | 5 | // CreateDocsURL creates a complete URL to the documentation for a rule. 6 | func CreateDocsURL(category, title string) string { 7 | return docsBaseURL + "/" + category + "/" + title 8 | } 9 | -------------------------------------------------------------------------------- /internal/embeds/embeds.go: -------------------------------------------------------------------------------- 1 | package embeds 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed templates 8 | var EmbedTemplatesFS embed.FS 9 | 10 | //go:embed schemas 11 | var SchemasFS embed.FS 12 | -------------------------------------------------------------------------------- /internal/embeds/templates/builtin/builtin.md.tpl: -------------------------------------------------------------------------------- 1 | # {{.NameOriginal}} 2 | 3 | **Summary**: ADD DESCRIPTION HERE 4 | 5 | **Category**: {{.Category | ToUpper}} 6 | 7 | **Automatically fixable**: Yes/No 8 | 9 | **Avoid** 10 | ```rego 11 | package policy 12 | 13 | # ... ADD CODE TO AVOID HERE 14 | ``` 15 | 16 | **Prefer** 17 | ```rego 18 | package policy 19 | 20 | # ... ADD CODE TO PREFER HERE 21 | ``` 22 | 23 | ## Rationale 24 | 25 | ADD RATIONALE HERE 26 | 27 | ## Configuration Options 28 | 29 | This linter rule provides the following configuration options: 30 | 31 | ```yaml 32 | rules: 33 | {{.Category}}: 34 | {{.NameOriginal}}: 35 | # one of "error", "warning", "ignore" 36 | level: error 37 | ``` 38 | 39 | ## Community 40 | 41 | If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, 42 | or just talk about Regal in general, please join us in the `#regal` channel in the Styra Community 43 | [Slack](https://inviter.co/styra)! 44 | -------------------------------------------------------------------------------- /internal/embeds/templates/builtin/builtin.rego.tpl: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Add description of rule here! 3 | package regal.rules.{{.Category}}{{.Name}} 4 | 5 | import data.regal.ast 6 | import data.regal.result 7 | 8 | report contains violation if { 9 | # Or change to imports, packages, comments, etc. 10 | some rule in input.rules 11 | 12 | # Deny any rule named foo, bar, or baz. This is just an example! 13 | # Add your own rule logic here. 14 | ast.ref_to_string(rule.head.ref) in {"foo", "bar", "baz"} 15 | 16 | violation := result.fail(rego.metadata.chain(), result.location(rule)) 17 | } 18 | -------------------------------------------------------------------------------- /internal/embeds/templates/builtin/builtin_test.rego.tpl: -------------------------------------------------------------------------------- 1 | package regal.rules.{{.Category}}{{.NameTest}} 2 | 3 | import data.regal.ast 4 | import data.regal.config 5 | 6 | import data.regal.rules.{{.Category}}{{.Name}} as rule 7 | 8 | # Example test, replace with your own 9 | test_rule_named_foo_not_allowed if { 10 | module := ast.policy("foo := true") 11 | 12 | r := rule.report with input as module 13 | 14 | # Use print(r) here to see the report. Great for development! 15 | 16 | r == {{ "{{" }} 17 | "category": "{{.Category}}", 18 | "description": "Add description of rule here!", 19 | "level": "error", 20 | "location": { 21 | "file": "policy.rego", 22 | "row": 1, 23 | "col": 1, 24 | "end": { 25 | "row": 1, 26 | "col": 12, 27 | }, 28 | "text": "foo := true", 29 | }, 30 | "related_resources": [{ 31 | "description": "documentation", 32 | "ref": config.docs.resolve_url("$baseUrl/$category/{{.NameOriginal}}", "{{.Category}}"), 33 | }], 34 | "title": "{{.NameOriginal}}", 35 | }} 36 | } 37 | -------------------------------------------------------------------------------- /internal/embeds/templates/custom/custom.rego.tpl: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: Add description of rule here! 3 | # schemas: 4 | # - input: schema.regal.ast 5 | package custom.regal.rules.{{.Category}}{{.Name}} 6 | 7 | import data.regal.ast 8 | import data.regal.result 9 | 10 | report contains violation if { 11 | # Or change to imports, packages, comments, etc. 12 | some rule in input.rules 13 | 14 | # Deny any rule named foo, bar, or baz. This is just an example! 15 | # Add your own rule logic here. 16 | ast.ref_to_string(rule.head.ref) in {"foo", "bar", "baz"} 17 | 18 | violation := result.fail(rego.metadata.chain(), result.location(rule)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/embeds/templates/custom/custom_test.rego.tpl: -------------------------------------------------------------------------------- 1 | package custom.regal.rules.{{.Category}}{{.NameTest}} 2 | 3 | import data.custom.regal.rules.{{.Category}}{{.Name}} as rule 4 | 5 | # Example test, replace with your own 6 | test_rule_named_foo_not_allowed if { 7 | module := regal.parse_module("example.rego", ` 8 | package policy 9 | 10 | foo := true`) 11 | 12 | r := rule.report with input as module 13 | 14 | # Use print(r) here to see the report. Great for development! 15 | 16 | r == {{ "{{" }} 17 | "category": "{{.Category}}", 18 | "description": "Add description of rule here!", 19 | "level": "error", 20 | "location": { 21 | "file": "example.rego", 22 | "row": 4, 23 | "col": 2, 24 | "end": { 25 | "row": 4, 26 | "col": 13, 27 | }, 28 | "text": "\tfoo := true" 29 | }, 30 | "title": "{{.NameOriginal}}", 31 | }} 32 | } 33 | -------------------------------------------------------------------------------- /internal/io/io_test.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/open-policy-agent/opa/v1/util/test" 8 | ) 9 | 10 | func TestFindManifestLocations(t *testing.T) { 11 | t.Parallel() 12 | 13 | fs := map[string]string{ 14 | "/.git": "", 15 | "/foo/bar/baz/.manifest": "", 16 | "/foo/bar/qux/.manifest": "", 17 | "/foo/bar/.regal/.manifest.yaml": "", 18 | "/node_modules/.manifest": "", 19 | } 20 | 21 | test.WithTempFS(fs, func(root string) { 22 | locations, err := FindManifestLocations(root) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | if len(locations) != 2 { 28 | t.Errorf("expected 2 locations, got %d", len(locations)) 29 | } 30 | 31 | expected := []string{"foo/bar/baz", "foo/bar/qux"} 32 | 33 | if !slices.Equal(locations, expected) { 34 | t.Errorf("expected %v, got %v", expected, locations) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/lsp/bundles/bundles.go: -------------------------------------------------------------------------------- 1 | package bundles 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | 9 | "github.com/open-policy-agent/opa/v1/bundle" 10 | ) 11 | 12 | // LoadDataBundle loads a bundle from the given path but only includes data 13 | // files. The path must contain a bundle manifest file. 14 | func LoadDataBundle(path string) (bundle.Bundle, error) { 15 | if _, err := os.Stat(filepath.Join(path, ".manifest")); err != nil { 16 | return bundle.Bundle{}, fmt.Errorf("manifest file was not found at bundle path %q", path) 17 | } 18 | 19 | b, err := bundle.NewCustomReader(bundle.NewDirectoryLoader(path).WithFilter(dataFileLoaderFilter)).Read() 20 | if err != nil { 21 | return bundle.Bundle{}, fmt.Errorf("failed to read bundle: %w", err) 22 | } 23 | 24 | return b, nil 25 | } 26 | 27 | func dataFileLoaderFilter(abspath string, info os.FileInfo, _ int) bool { 28 | if info.IsDir() { 29 | return false 30 | } 31 | 32 | basename := filepath.Base(abspath) 33 | 34 | return !slices.Contains([]string{".manifest", "data.json", "data.yml", "data.yaml"}, basename) 35 | } 36 | -------------------------------------------------------------------------------- /internal/lsp/clients/clients.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | // Identifier represent different supported clients and can be used to toggle or change 4 | // server behavior based on the client. 5 | type Identifier int 6 | 7 | const ( 8 | IdentifierGeneric Identifier = iota 9 | IdentifierVSCode 10 | IdentifierGoTest 11 | IdentifierZed 12 | IdentifierNeovim 13 | ) 14 | 15 | // DetermineClientIdentifier is used to determine the Regal client identifier 16 | // based on the client name. 17 | // Clients with identifiers here should be featured on the 'Editor Support' 18 | // page in the documentation (https://docs.styra.com/regal/editor-support). 19 | func DetermineClientIdentifier(clientName string) Identifier { 20 | switch clientName { 21 | case "go test": 22 | return IdentifierGoTest 23 | case "Visual Studio Code": 24 | return IdentifierVSCode 25 | case "Zed": 26 | return IdentifierZed 27 | case "Neovim": 28 | // 'Neovim' is sent as the client identifier when using the 29 | // nvim-lspconfig plugin. 30 | return IdentifierNeovim 31 | } 32 | 33 | return IdentifierGeneric 34 | } 35 | -------------------------------------------------------------------------------- /internal/lsp/completions/manager_test.go: -------------------------------------------------------------------------------- 1 | package completions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | 8 | "github.com/styrainc/regal/internal/lsp/cache" 9 | "github.com/styrainc/regal/internal/lsp/completions/providers" 10 | "github.com/styrainc/regal/internal/lsp/types" 11 | ) 12 | 13 | func TestManagerEarlyExitInsideComment(t *testing.T) { 14 | t.Parallel() 15 | 16 | c := cache.NewCache() 17 | fileURI := "file:///foo/bar/file.rego" 18 | 19 | fileContents := `package p 20 | 21 | # foo := http 22 | ` 23 | 24 | module := ast.MustParseModule(fileContents) 25 | 26 | c.SetFileContents(fileURI, fileContents) 27 | c.SetModule(fileURI, module) 28 | 29 | mgr := NewManager(c, &ManagerOptions{}) 30 | mgr.RegisterProvider(&providers.BuiltIns{}) 31 | 32 | completionParams := types.CompletionParams{ 33 | TextDocument: types.TextDocumentIdentifier{ 34 | URI: fileURI, 35 | }, 36 | Position: types.Position{ 37 | Line: 2, 38 | Character: 13, 39 | }, 40 | } 41 | 42 | completions, err := mgr.Run(t.Context(), completionParams, nil) 43 | if err != nil { 44 | t.Fatalf("Unexpected error: %v", err) 45 | } 46 | 47 | if len(completions) != 0 { 48 | t.Errorf("Expected no completions, got: %v", completions) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/lsp/completions/providers/options.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "github.com/open-policy-agent/opa/v1/ast" 5 | 6 | "github.com/styrainc/regal/internal/lsp/clients" 7 | ) 8 | 9 | type Options struct { 10 | // Builtins is a map of built-in functions to their definitions required in 11 | // the context of the current completion request. 12 | Builtins map[string]*ast.Builtin 13 | RootURI string 14 | ClientIdentifier clients.Identifier 15 | RegoVersion ast.RegoVersion 16 | } 17 | -------------------------------------------------------------------------------- /internal/lsp/completions/providers/shared_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | // testCaseFileURI is used in various tests in the providers package. 4 | const testCaseFileURI = "file:///foo/bar/file.rego" 5 | -------------------------------------------------------------------------------- /internal/lsp/completions/refs/used_test.go: -------------------------------------------------------------------------------- 1 | package refs 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | rparse "github.com/styrainc/regal/internal/parse" 8 | ) 9 | 10 | func TestUsedInModule(t *testing.T) { 11 | t.Parallel() 12 | 13 | mod := rparse.MustParseModule(` 14 | package example 15 | 16 | import data.foo as wow 17 | import data.bar 18 | 19 | allow if input.user == "admin" 20 | 21 | allow if data.users.admin == input.user 22 | 23 | deny contains wow.password if { 24 | input.magic == true 25 | } 26 | 27 | deny contains input.parrot if { 28 | bar.parrot != "a bird" 29 | } 30 | `) 31 | 32 | items, err := UsedInModule(t.Context(), mod) 33 | if err != nil { 34 | t.Fatalf("Unexpected error: %s", err) 35 | } 36 | 37 | expectedItems := []string{ 38 | "wow", 39 | "bar", 40 | "bar.parrot", 41 | "data.users.admin", 42 | "input.magic", 43 | "input.parrot", 44 | "input.user", 45 | "wow.password", 46 | } 47 | 48 | for _, item := range expectedItems { 49 | if !slices.Contains(items, item) { 50 | t.Errorf("Expected item %q not found in items", item) 51 | } 52 | } 53 | 54 | for _, item := range items { 55 | if !slices.Contains(expectedItems, item) { 56 | t.Errorf("Unexpected item %q found in items", item) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/lsp/config/find.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // FindConfigRoots will search for all config roots in the given path. A config 11 | // root is a directory that contains a .regal.yaml file or a .regal/config.yaml 12 | // file. This is intended to be used to by the language server when determining 13 | // the most suitable config root for the server to operate on. 14 | func FindConfigRoots(path string) ([]string, error) { 15 | var foundRoots []string 16 | 17 | err := filepath.WalkDir(path, func(path string, info os.DirEntry, err error) error { 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if info.IsDir() || !strings.HasSuffix(path, ".yaml") { 23 | return nil 24 | } 25 | 26 | if filepath.Base(path) == ".regal.yaml" { 27 | foundRoots = append(foundRoots, filepath.Dir(path)) 28 | } 29 | 30 | if strings.HasSuffix(path, ".regal/config.yaml") { 31 | foundRoots = append(foundRoots, filepath.Dir(filepath.Dir(path))) 32 | } 33 | 34 | return nil 35 | }) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to walk directory: %w", err) 38 | } 39 | 40 | return foundRoots, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/lsp/examples/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": { 3 | "contains": "builtins/contains", 4 | "io.jwt.decode_verify": "builtins/io_jwt/decode_verify", 5 | "print": "builtins/print", 6 | "regex.find_all_string_submatch_n": "builtins/regex/find_all_string_submatch_n", 7 | "regex.globs_match": "builtins/regex/globs_match", 8 | "regex.match": "builtins/regex/match", 9 | "regex.template_match": "builtins/regex/template_match", 10 | "time.clock": "builtins/time/clock", 11 | "time.format": "builtins/time/format", 12 | "time.now_ns": "builtins/time/now_ns", 13 | "time.parse_ns": "builtins/time/parse_ns" 14 | }, 15 | "keywords": { 16 | "contains": "keywords/contains", 17 | "default": "keywords/default", 18 | "if": "keywords/if", 19 | "import": "keywords/import", 20 | "some": "keywords/some" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/lsp/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sourcegraph/jsonrpc2" 7 | 8 | "github.com/styrainc/roast/pkg/encoding" 9 | ) 10 | 11 | type handlerFunc[T any] func(T) (any, error) 12 | 13 | type handlerContextFunc[T any] func(context.Context, T) (any, error) 14 | 15 | var ErrInvalidParams = &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 16 | 17 | func Decode[T any](req *jsonrpc2.Request, params *T) error { 18 | if req.Params == nil { 19 | return ErrInvalidParams 20 | } 21 | 22 | if err := encoding.JSON().Unmarshal(*req.Params, ¶ms); err != nil { 23 | return &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams, Message: err.Error()} 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func WithParams[T any](req *jsonrpc2.Request, h handlerFunc[T]) (any, error) { 30 | var params T 31 | if err := Decode(req, ¶ms); err != nil { 32 | return nil, err 33 | } 34 | 35 | return h(params) 36 | } 37 | 38 | func WithContextAndParams[T any](ctx context.Context, req *jsonrpc2.Request, h handlerContextFunc[T]) (any, error) { 39 | var params T 40 | if err := Decode(req, ¶ms); err != nil { 41 | return nil, err 42 | } 43 | 44 | return h(ctx, params) 45 | } 46 | -------------------------------------------------------------------------------- /internal/lsp/hover/testdata/hover/foobar.md: -------------------------------------------------------------------------------- 1 | ### [foo.bar](https://example.com) 2 | 3 | ```rego 4 | output := foo.bar(arg1, arg2) 5 | ``` 6 | 7 | Description for Foo Bar 8 | 9 | 10 | #### Arguments 11 | 12 | - `arg1` string — arg1 for foobar 13 | - `arg2` string — arg2 for foobar 14 | 15 | 16 | Returns `output` of type `number`: the output for foobar 17 | -------------------------------------------------------------------------------- /internal/lsp/hover/testdata/hover/graphreachable.md: -------------------------------------------------------------------------------- 1 | ### [graph.reachable](https://www.openpolicyagent.org/docs/policy-reference/#builtin-graph-graphreachable) 2 | 3 | ```rego 4 | output := graph.reachable(graph, initial) 5 | ``` 6 | 7 | Computes the set of reachable nodes in the graph from a set of starting nodes. 8 | 9 | 10 | #### Arguments 11 | 12 | - `graph` object[any: any] — object containing a set or array of neighboring vertices 13 | - `initial` any — set or array of root vertices 14 | 15 | 16 | Returns `output` of type `set[any]`: set of vertices reachable from the `initial` vertices in the directed `graph` 17 | -------------------------------------------------------------------------------- /internal/lsp/hover/testdata/hover/indexof.md: -------------------------------------------------------------------------------- 1 | ### [indexof](https://www.openpolicyagent.org/docs/policy-reference/#builtin-strings-indexof) 2 | 3 | ```rego 4 | output := indexof(haystack, needle) 5 | ``` 6 | 7 | Returns the index of a substring contained inside a string. 8 | 9 | 10 | #### Arguments 11 | 12 | - `haystack` string — string to search in 13 | - `needle` string — substring to look for 14 | 15 | 16 | Returns `output` of type `number`: index of first occurrence, `-1` if not found 17 | -------------------------------------------------------------------------------- /internal/lsp/hover/testdata/hover/jsonfilter.md: -------------------------------------------------------------------------------- 1 | ### [json.filter](https://www.openpolicyagent.org/docs/policy-reference/#builtin-object-jsonfilter) 2 | 3 | ```rego 4 | filtered := json.filter(object, paths) 5 | ``` 6 | 7 | Filters the object. For example: `json.filter({"a": {"b": "x", "c": "y"}}, ["a/b"])` will result in `{"a": {"b": "x"}}`). Paths are not filtered in-order and are deduplicated before being evaluated. 8 | 9 | 10 | #### Arguments 11 | 12 | - `object` object[any: any] — object to filter 13 | - `paths` any], set[any]> — JSON string paths 14 | 15 | 16 | Returns `filtered` of type `any`: remaining data from `object` with only keys specified in `paths` 17 | -------------------------------------------------------------------------------- /internal/lsp/log/level.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // Level controls the level of server logging and corresponds to TraceValue in 4 | // the LSP spec: 5 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue. 6 | type Level int 7 | 8 | const ( 9 | // LevelOff is used to disable logging completely. 10 | LevelOff Level = iota 11 | // LogLevelMessage are intended to contain errors and other messages that 12 | // should be shown in normal operation. 13 | LevelMessage 14 | // LogLevelDebug is includes LogLevelMessage, but also information that is 15 | // not expected to be useful unless debugging the server. 16 | LevelDebug 17 | ) 18 | 19 | func (l Level) String() string { 20 | return [...]string{"Off", "Messages", "Debug"}[l] 21 | } 22 | 23 | func (l Level) ShouldLog(incoming Level) bool { 24 | if l == LevelOff { 25 | return false 26 | } 27 | 28 | if l == LevelDebug { 29 | return true 30 | } 31 | 32 | return l == LevelMessage && incoming == LevelMessage 33 | } 34 | -------------------------------------------------------------------------------- /internal/lsp/log/level_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "testing" 4 | 5 | func TestShouldLog(t *testing.T) { 6 | t.Parallel() 7 | 8 | testCases := map[string]struct { 9 | Level Level 10 | IncomingMessageLevel Level 11 | ExpectLog bool 12 | }{ 13 | "Off, no logs": { 14 | Level: LevelOff, 15 | IncomingMessageLevel: LevelMessage, 16 | ExpectLog: false, 17 | }, 18 | "Off, no logs from debug": { 19 | Level: LevelOff, 20 | IncomingMessageLevel: LevelDebug, 21 | ExpectLog: false, 22 | }, 23 | "Message, no logs from debug": { 24 | Level: LevelMessage, 25 | IncomingMessageLevel: LevelDebug, 26 | ExpectLog: false, 27 | }, 28 | "Debug, logs from Message": { 29 | Level: LevelDebug, 30 | IncomingMessageLevel: LevelMessage, 31 | ExpectLog: true, 32 | }, 33 | "Debug, all logs": { 34 | Level: LevelDebug, 35 | IncomingMessageLevel: LevelDebug, 36 | ExpectLog: true, 37 | }, 38 | } 39 | 40 | for label, tc := range testCases { 41 | t.Run(label, func(t *testing.T) { 42 | t.Parallel() 43 | 44 | if got := tc.Level.ShouldLog(tc.IncomingMessageLevel); got != tc.ExpectLog { 45 | t.Errorf("expected %v, got %v", tc.ExpectLog, got) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/lsp/race_off.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package lsp 5 | 6 | func isRaceEnabled() bool { 7 | return false 8 | } 9 | -------------------------------------------------------------------------------- /internal/lsp/race_on.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | // +build race 3 | 4 | package lsp 5 | 6 | func isRaceEnabled() bool { 7 | return true 8 | } 9 | -------------------------------------------------------------------------------- /internal/lsp/rego/builtins.go: -------------------------------------------------------------------------------- 1 | package rego 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | ) 8 | 9 | // BuiltinsForCapabilities returns a list of builtins from the provided capabilities. 10 | func BuiltinsForCapabilities(capabilities *ast.Capabilities) map[string]*ast.Builtin { 11 | m := make(map[string]*ast.Builtin) 12 | for _, b := range capabilities.Builtins { 13 | m[b.Name] = b 14 | } 15 | 16 | return m 17 | } 18 | 19 | func BuiltinCategory(builtin *ast.Builtin) (category string) { 20 | if len(builtin.Categories) == 0 { 21 | if s := strings.Split(builtin.Name, "."); len(s) > 1 { 22 | category = s[0] 23 | } else { 24 | category = builtin.Name 25 | } 26 | } else { 27 | category = builtin.Categories[0] 28 | } 29 | 30 | return category 31 | } 32 | -------------------------------------------------------------------------------- /internal/lsp/server_builtins_test.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/styrainc/regal/internal/lsp/log" 7 | ) 8 | 9 | // https://github.com/StyraInc/regal/issues/679 10 | func TestProcessBuiltinUpdateExitsOnMissingFile(t *testing.T) { 11 | t.Parallel() 12 | 13 | logger := newTestLogger(t) 14 | 15 | ls := NewLanguageServer( 16 | t.Context(), 17 | &LanguageServerOptions{LogWriter: logger, LogLevel: log.LevelDebug}, 18 | ) 19 | 20 | if err := ls.processHoverContentUpdate(t.Context(), "file://missing.rego", "foo"); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if l := len(ls.cache.GetAllBuiltInPositions()); l != 0 { 25 | t.Errorf("expected builtin positions to be empty, got %d items", l) 26 | } 27 | 28 | contents, ok := ls.cache.GetFileContents("file://missing.rego") 29 | if ok { 30 | t.Errorf("expected file contents to be empty, got %s", contents) 31 | } 32 | 33 | if len(ls.cache.GetAllFiles()) != 0 { 34 | t.Errorf("expected files to be empty, got %v", ls.cache.GetAllFiles()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/lsp/stdio.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type StdOutReadWriteCloser struct{} 9 | 10 | func (StdOutReadWriteCloser) Read(p []byte) (int, error) { 11 | c, err := os.Stdin.Read(p) 12 | if err != nil { 13 | return c, fmt.Errorf("failed to read from stdin: %w", err) 14 | } 15 | 16 | return c, nil 17 | } 18 | 19 | func (StdOutReadWriteCloser) Write(p []byte) (int, error) { 20 | c, err := os.Stdout.Write(p) 21 | if err != nil { 22 | return c, fmt.Errorf("failed to write to stdout: %w", err) 23 | } 24 | 25 | return c, nil 26 | } 27 | 28 | func (StdOutReadWriteCloser) Close() error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/lsp/types/completion/completion.go: -------------------------------------------------------------------------------- 1 | package completion 2 | 3 | type ItemKind uint 4 | 5 | const ( 6 | Text ItemKind = iota + 1 7 | Method 8 | Function 9 | Constructor 10 | Field 11 | Variable 12 | Class 13 | Interface 14 | Module 15 | Property 16 | Unit 17 | Value 18 | Enum 19 | Keyword 20 | Snippet 21 | Color 22 | File 23 | Reference 24 | Folder 25 | EnumMember 26 | Constant 27 | Struct 28 | Event 29 | Operator 30 | TypeParameter 31 | ) 32 | 33 | type TriggerKind uint 34 | 35 | const ( 36 | Invoked TriggerKind = iota + 1 37 | TriggerCharacter 38 | TriggerForIncompleteCompletions 39 | ) 40 | -------------------------------------------------------------------------------- /internal/lsp/types/internal.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/open-policy-agent/opa/v1/ast" 4 | 5 | // Ref is a generic construct for an object found in a Rego module. 6 | // Ref is designed to be used in completions and provides information 7 | // relevant to the object with that operation in mind. 8 | type Ref struct { 9 | // Label is a identifier for the object. e.g. data.package.rule. 10 | Label string `json:"label"` 11 | // Detail is a small amount of additional information about the object. 12 | Detail string `json:"detail"` 13 | // Description is a longer description of the object and uses Markdown formatting. 14 | Description string `json:"description"` 15 | Kind RefKind `json:"kind"` 16 | } 17 | 18 | // RefKind represents the kind of object that a Ref represents. 19 | // This is intended to toggle functionality and which UI symbols to use. 20 | type RefKind int 21 | 22 | const ( 23 | Package RefKind = iota + 1 24 | Rule 25 | ConstantRule 26 | Function 27 | ) 28 | 29 | type BuiltinPosition struct { 30 | Builtin *ast.Builtin 31 | Line uint 32 | Start uint 33 | End uint 34 | } 35 | 36 | type KeywordLocation struct { 37 | Name string 38 | Line uint 39 | Start uint 40 | End uint 41 | } 42 | -------------------------------------------------------------------------------- /internal/lsp/types/symbols/symbols.go: -------------------------------------------------------------------------------- 1 | package symbols 2 | 3 | type SymbolKind int 4 | 5 | const ( 6 | File SymbolKind = iota + 1 7 | Module 8 | Namespace 9 | Package 10 | Class 11 | Method 12 | Property 13 | Field 14 | Constructor 15 | Enum 16 | Interface 17 | Function 18 | Variable 19 | Constant 20 | String 21 | Number 22 | Boolean 23 | Array 24 | Object 25 | Key 26 | Null 27 | EnumMember 28 | Struct 29 | Event 30 | Operator 31 | TypeParameter 32 | ) 33 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/open-policy-agent/opa/v1/profiler" 5 | 6 | "github.com/styrainc/regal/pkg/report" 7 | ) 8 | 9 | const ( 10 | RegalConfigSearch = "regal_config_search" 11 | RegalConfigParse = "regal_config_parse" 12 | RegalFilterIgnoredFiles = "regal_filter_ignored_files" 13 | RegalFilterIgnoredModules = "regal_filter_ignored_modules" 14 | RegalInputParse = "regal_input_parse" 15 | RegalLint = "regal_lint_total" 16 | RegalLintRego = "regal_lint_rego" 17 | RegalLintRegoAggregate = "regal_lint_rego_aggregate" 18 | ) 19 | 20 | func FromExprStats(stats profiler.ExprStats) report.ProfileEntry { 21 | return report.ProfileEntry{ 22 | Location: stats.Location.String(), 23 | TotalTimeNs: stats.ExprTimeNs, 24 | NumEval: stats.NumEval, 25 | NumRedo: stats.NumRedo, 26 | NumGenExpr: stats.NumGenExpr, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/mode/standalone.go: -------------------------------------------------------------------------------- 1 | //go:build !regal_standalone 2 | 3 | package mode 4 | 5 | // Standalone lets us change the output of some commands when Regal 6 | // is used as a binary, as opposed to when it's embedded via its 7 | // Go module. 8 | const Standalone = false 9 | -------------------------------------------------------------------------------- /internal/mode/standalone_flag.go: -------------------------------------------------------------------------------- 1 | //go:build regal_standalone 2 | 3 | package mode 4 | 5 | const Standalone = true 6 | -------------------------------------------------------------------------------- /internal/novelty/novelty.go: -------------------------------------------------------------------------------- 1 | //go:build !regal_enable_novelty 2 | 3 | package novelty 4 | 5 | func HappyHolidays() error { return nil } 6 | -------------------------------------------------------------------------------- /internal/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/open-policy-agent/opa/v1/ast" 5 | 6 | "github.com/styrainc/regal/internal/parse" 7 | "github.com/styrainc/regal/pkg/rules" 8 | ) 9 | 10 | func InputPolicy(filename string, policy string) rules.Input { 11 | content := map[string]string{filename: policy} 12 | modules := map[string]*ast.Module{filename: parse.MustParseModule(policy)} 13 | 14 | return rules.NewInput(content, modules) 15 | } 16 | -------------------------------------------------------------------------------- /internal/util/ast.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | ) 8 | 9 | // UnquotedPath returns a slice of strings from a path without quotes. 10 | // e.g. data.foo["bar"] -> ["foo", "bar"], note that the data is not included. 11 | func UnquotedPath(path ast.Ref) []string { 12 | ret := make([]string, 0, len(path)-1) 13 | for _, ref := range path[1:] { 14 | ret = append(ret, strings.Trim(ref.String(), `"`)) 15 | } 16 | 17 | return ret 18 | } 19 | -------------------------------------------------------------------------------- /internal/web/assets/main.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 19 |
20 |
21 |
22 | {{ block "output" . }} 23 | {{ range .Result }} 24 |
25 | {{ .Stage }} 26 |
{{ .Output }}
27 |
28 | {{ end }} 29 | {{ end }} 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /internal/web/server_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestTemplateFoundAndParsed(t *testing.T) { 10 | t.Parallel() 11 | 12 | buf := bytes.Buffer{} 13 | 14 | st := state{Code: "package main\n\nimport rego.v1\n"} 15 | 16 | if err := tpl.ExecuteTemplate(&buf, mainTemplate, st); err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | if !strings.HasPrefix(buf.String(), "") { 21 | t.Fatalf("expected HTML document, got %s", buf.String()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | 8 | "github.com/styrainc/regal/cmd" 9 | ) 10 | 11 | func main() { 12 | // Remove date and time from any `log.*` calls, as that doesn't add much of value here 13 | // Evaluate options for logging later 14 | log.SetFlags(0) 15 | 16 | if err := cmd.RootCommand.Execute(); err != nil { 17 | code := 1 18 | if e := (cmd.ExitError{}); errors.As(err, &e) { 19 | code = e.Code() 20 | } 21 | 22 | os.Exit(code) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/builtins/builtins_test.go: -------------------------------------------------------------------------------- 1 | package builtins_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | "github.com/open-policy-agent/opa/v1/rego" 8 | 9 | "github.com/styrainc/regal/pkg/builtins" 10 | ) 11 | 12 | // Can't be much faster than this.. 13 | // BenchmarkRegalLast-10 163252460 7.218 ns/op 0 B/op 0 allocs/op 14 | // ... 15 | func BenchmarkRegalLast(b *testing.B) { 16 | bctx := rego.BuiltinContext{} 17 | ta, tb, tc := ast.StringTerm("a"), ast.StringTerm("b"), ast.StringTerm("c") 18 | arr := ast.ArrayTerm(ta, tb, tc) 19 | 20 | var res *ast.Term 21 | 22 | for range b.N { 23 | var err error 24 | 25 | res, err = builtins.RegalLast(bctx, arr) 26 | if err != nil { 27 | b.Fatal(err) 28 | } 29 | } 30 | 31 | if res.Value.Compare(tc.Value) != 0 { 32 | b.Fatalf("expected c, got %v", res) 33 | } 34 | } 35 | 36 | // Likewise for the empty array case. 37 | // BenchmarkRegalLastEmptyArr-10 160589398 7.498 ns/op 0 B/op 0 allocs/op 38 | // ... 39 | func BenchmarkRegalLastEmptyArr(b *testing.B) { 40 | bctx := rego.BuiltinContext{} 41 | arr := ast.ArrayTerm() 42 | 43 | var err error 44 | 45 | for range b.N { 46 | _, err = builtins.RegalLast(bctx, arr) 47 | } 48 | 49 | if err == nil { 50 | b.Fatal("expected error, got nil") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/config/fixtures/caps.json: -------------------------------------------------------------------------------- 1 | { 2 | "builtins": [ 3 | { 4 | "name": "wow", 5 | "description": "Increases wow in Rego rule", 6 | "categories": [ 7 | "special" 8 | ], 9 | "decl": { 10 | "args": [ 11 | { 12 | "name": "x", 13 | "type": "number" 14 | } 15 | ], 16 | "result": { 17 | "description": "the wowness level for input `x`", 18 | "name": "y", 19 | "type": "number" 20 | }, 21 | "type": "function" 22 | } 23 | } 24 | ], 25 | "future_keywords": [ 26 | "contains", 27 | "every", 28 | "if", 29 | "in" 30 | ], 31 | "wasm_abi_versions": null, 32 | "features": [ 33 | "rule_head_ref_string_prefixes" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /pkg/config/global.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // GlobalConfigDir is the config directory that will be used for user-wide 9 | // configuration. This is different from the .regal directories that are 10 | // searched for when linting. If create is false, the function will return an 11 | // empty string if the directory does not exist. 12 | func GlobalConfigDir(create bool) string { 13 | cfgDir, err := os.UserHomeDir() 14 | if err != nil { 15 | return "" 16 | } 17 | 18 | regalDir := filepath.Join(cfgDir, ".config", "regal") 19 | if _, err := os.Stat(regalDir); os.IsNotExist(err) { 20 | if !create { 21 | return "" 22 | } 23 | 24 | if err := os.Mkdir(regalDir, os.ModePerm); err != nil { 25 | return "" 26 | } 27 | } 28 | 29 | return regalDir 30 | } 31 | -------------------------------------------------------------------------------- /pkg/fixer/fileprovider/fp.go: -------------------------------------------------------------------------------- 1 | package fileprovider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/open-policy-agent/opa/v1/ast" 7 | 8 | "github.com/styrainc/regal/pkg/rules" 9 | ) 10 | 11 | type FileProvider interface { 12 | List() ([]string, error) 13 | 14 | Get(string) (string, error) 15 | Put(string, string) error 16 | Delete(string) error 17 | Rename(string, string) error 18 | 19 | ToInput(versionsMap map[string]ast.RegoVersion) (rules.Input, error) 20 | } 21 | 22 | type RenameConflictError struct { 23 | From string 24 | To string 25 | } 26 | 27 | func (e RenameConflictError) Error() string { 28 | return fmt.Sprintf("rename conflict: %q cannot be renamed as the target location %q already exists", e.From, e.To) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/fixer/fileprovider/inmem_test.go: -------------------------------------------------------------------------------- 1 | package fileprovider 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/styrainc/regal/internal/testutil" 8 | ) 9 | 10 | func TestFromFS(t *testing.T) { 11 | t.Parallel() 12 | 13 | tempDir := testutil.TempDirectoryOf(t, map[string]string{ 14 | "foo/bar/baz": "bar", 15 | "bar/foo": "baz", 16 | }) 17 | fp := testutil.Must(NewInMemoryFileProviderFromFS([]string{ 18 | filepath.Join(tempDir, "foo", "bar", "baz"), 19 | filepath.Join(tempDir, "bar", "foo"), 20 | }...))(t) 21 | 22 | if fc, err := fp.Get(filepath.Join(tempDir, "foo", "bar", "baz")); err != nil || fc != "bar" { 23 | t.Fatalf("expected %s, got %s", "bar", fc) 24 | } 25 | } 26 | 27 | func TestRenameConflict(t *testing.T) { 28 | t.Parallel() 29 | 30 | fp := NewInMemoryFileProvider(map[string]string{ 31 | "/foo/bar/baz": "bar", 32 | "/bar/foo": "baz", 33 | }) 34 | 35 | err := fp.Rename("/foo/bar/baz", "/bar/foo") 36 | if err == nil { 37 | t.Fatal("expected error") 38 | } 39 | 40 | exp := `rename conflict: "/foo/bar/baz" cannot be renamed as the target location "/bar/foo" already exists` 41 | 42 | if got := err.Error(); got != exp { 43 | t.Fatalf("expected %s, got %s", exp, got) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/fixer/fixes/nowhitespacecomment.go: -------------------------------------------------------------------------------- 1 | package fixes 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type NoWhitespaceComment struct{} 9 | 10 | func (*NoWhitespaceComment) Name() string { 11 | return "no-whitespace-comment" 12 | } 13 | 14 | func (n *NoWhitespaceComment) Fix(fc *FixCandidate, opts *RuntimeOptions) ([]FixResult, error) { 15 | lines := strings.Split(fc.Contents, "\n") 16 | 17 | if opts == nil { 18 | return nil, errors.New("missing runtime options") 19 | } 20 | 21 | fixed := false 22 | 23 | for _, loc := range opts.Locations { 24 | // unexpected line in file, skipping 25 | if loc.Row > len(lines) { 26 | continue 27 | } 28 | 29 | if loc.Column > len(lines[loc.Row-1]) || loc.Column < 1 { 30 | continue 31 | } 32 | 33 | line := lines[loc.Row-1] 34 | 35 | // unexpected character at location column, skipping 36 | if line[loc.Column-1] != byte('#') { 37 | continue 38 | } 39 | 40 | lines[loc.Row-1] = line[0:loc.Column] + " " + line[loc.Column:] 41 | fixed = true 42 | } 43 | 44 | if !fixed { 45 | return nil, nil 46 | } 47 | 48 | return []FixResult{{ 49 | Title: n.Name(), 50 | Root: opts.BaseDir, 51 | Contents: strings.Join(lines, "\n"), 52 | }}, nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/fixer/fixes/useassignmentoperator.go: -------------------------------------------------------------------------------- 1 | package fixes 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type UseAssignmentOperator struct{} 9 | 10 | func (*UseAssignmentOperator) Name() string { 11 | return "use-assignment-operator" 12 | } 13 | 14 | func (u *UseAssignmentOperator) Fix(fc *FixCandidate, opts *RuntimeOptions) ([]FixResult, error) { 15 | lines := strings.Split(fc.Contents, "\n") 16 | 17 | if opts == nil { 18 | return nil, errors.New("missing runtime options") 19 | } 20 | 21 | fixed := false 22 | 23 | for _, loc := range opts.Locations { 24 | if loc.Row > len(lines) { 25 | continue 26 | } 27 | 28 | line := lines[loc.Row-1] 29 | 30 | if loc.Column-1 < 0 || loc.Column-1 >= len(line) { 31 | continue 32 | } 33 | 34 | // unexpected character at location column, skipping 35 | if line[loc.Column-1] != '=' { 36 | continue 37 | } 38 | 39 | lines[loc.Row-1] = line[0:loc.Column-1] + ":" + line[loc.Column-1:] 40 | fixed = true 41 | } 42 | 43 | if !fixed { 44 | return nil, nil 45 | } 46 | 47 | return []FixResult{{ 48 | Title: u.Name(), 49 | Root: opts.BaseDir, 50 | Contents: strings.Join(lines, "\n"), 51 | }}, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/fixer/rename.go: -------------------------------------------------------------------------------- 1 | package fixer 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // renameCandidate takes a filename and produces a new name with an incremented 12 | // numeric suffix. It correctly handles test files by inserting the increment 13 | // before the "_test" suffix and preserves the original directory. 14 | func renameCandidate(oldName string) string { 15 | dir := filepath.Dir(oldName) 16 | baseWithExt := filepath.Base(oldName) 17 | 18 | ext := filepath.Ext(baseWithExt) 19 | base := strings.TrimSuffix(baseWithExt, ext) 20 | 21 | suffix := "" 22 | if strings.HasSuffix(base, "_test") { 23 | suffix = "_test" 24 | base = strings.TrimSuffix(base, "_test") 25 | } 26 | 27 | re := regexp.MustCompile(`^(.*)_(\d+)$`) 28 | matches := re.FindStringSubmatch(base) 29 | 30 | if len(matches) == 3 { 31 | baseName := matches[1] 32 | numStr := matches[2] 33 | num, _ := strconv.Atoi(numStr) 34 | num++ 35 | base = fmt.Sprintf("%s_%d", baseName, num) 36 | } else { 37 | base += "_1" 38 | } 39 | 40 | newBase := base + suffix + ext 41 | newName := filepath.Join(dir, newBase) 42 | 43 | return newName 44 | } 45 | -------------------------------------------------------------------------------- /pkg/hints/hints_test.go: -------------------------------------------------------------------------------- 1 | package hints 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/styrainc/regal/internal/parse" 8 | ) 9 | 10 | func TestHints(t *testing.T) { 11 | t.Parallel() 12 | 13 | mod := `package foo 14 | 15 | incomplete` 16 | 17 | _, err := parse.Module("test.rego", mod) 18 | if err == nil { 19 | t.Fatal("expected error") 20 | } 21 | 22 | hints, err := GetForError(err) 23 | if err != nil { 24 | t.Fatalf("unexpected error: %s", err) 25 | } 26 | 27 | expectedHints := []string{"rego-parse-error/var-cannot-be-used-for-rule-name"} 28 | if !slices.Equal(hints, expectedHints) { 29 | t.Fatalf("expected\n%v but got\n%v", expectedHints, hints) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/linter/testdata/custom.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: All packages must use "acme.corp" base name 3 | # related_resources: 4 | # - description: documentation 5 | # ref: https://www.acmecorp.example.org/docs/regal/package 6 | package custom.regal.rules.naming["acme-corp-package"] 7 | 8 | import data.regal.result 9 | 10 | report contains violation if { 11 | not acme_corp_package 12 | not system_log_package 13 | 14 | violation := result.fail(rego.metadata.chain(), result.location(input["package"].path[1])) 15 | } 16 | 17 | acme_corp_package if { 18 | input["package"].path[1].value == "acme" 19 | input["package"].path[2].value == "corp" 20 | } 21 | 22 | system_log_package if { 23 | input["package"].path[1].value == "system" 24 | input["package"].path[2].value == "log" 25 | } 26 | -------------------------------------------------------------------------------- /pkg/linter/testdata/printer.rego: -------------------------------------------------------------------------------- 1 | # METADATA 2 | # description: All I ever do is print 3 | # related_resources: 4 | # - description: documentation 5 | # ref: https://www.acmecorp.example.org/docs/regal/package 6 | package custom.regal.rules.utils["printer"] 7 | 8 | report contains "never happens" if { 9 | print(input.regal.file.name) 10 | 11 | false 12 | } 13 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/json/reporter-no-violations.json: -------------------------------------------------------------------------------- 1 | { 2 | "violations": [], 3 | "summary": { 4 | "files_scanned": 0, 5 | "files_failed": 0, 6 | "rules_skipped": 0, 7 | "num_violations": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/junit/reporter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/pretty/reporter-long-text.txt: -------------------------------------------------------------------------------- 1 | Rule: long-violation 2 | Level: warning 3 | Description: violation with a long description 4 | Category: long 5 | Location: b.rego:22:18 6 | Text: long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,lo... 7 | Documentation: https://example.com/to-long 8 | 9 | 3 files linted. 1 violation (0 errors, 1 warning) found. 10 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/pretty/reporter.txt: -------------------------------------------------------------------------------- 1 | Rule: breaking-the-law 2 | Level: error 3 | Description: Rego must not break the law! 4 | Category: legal 5 | Location: a.rego:1:1 6 | Text: package illegal 7 | Documentation: https://example.com/illegal 8 | 9 | Rule: questionable-decision 10 | Level: warning 11 | Description: Questionable decision found 12 | Category: really? 13 | Location: b.rego:22:18 14 | Text: default allow = true 15 | Documentation: https://example.com/questionable 16 | 17 | 3 files linted. 2 violations (1 error, 1 warning) found in 2 files. 1 rule skipped: 18 | - rule-missing-capability: Rule missing capability bar 19 | 20 | -------------------------------------------------------------------------------- /pkg/reporter/testdata/sarif/reporter-no-violation.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "informationUri": "https://docs.styra.com/regal", 9 | "name": "Regal", 10 | "rules": [] 11 | } 12 | }, 13 | "results": [] 14 | } 15 | ] 16 | } --------------------------------------------------------------------------------