diff --git a/src/languages/rego/compiler/rules.rs b/src/languages/rego/compiler/rules.rs index 7227700..990bef3 100644 --- a/src/languages/rego/compiler/rules.rs +++ b/src/languages/rego/compiler/rules.rs @@ -38,6 +38,11 @@ impl<'a> Compiler<'a> { pub(super) fn compute_rule_type(&self, rule_path: &str) -> Result { let Some(definitions) = self.policy.inner.rules.get(rule_path) else { + // Default-only rules (e.g., `default deny := true`) have no regular definitions + // in the `rules` map — they only exist in `default_rules`. Treat them as Complete. + if self.policy.inner.default_rules.contains_key(rule_path) { + return Ok(RuleType::Complete); + } return Err(CompilerError::General { message: format!("no definitions found for rule path '{}'", rule_path), } @@ -614,6 +619,31 @@ impl<'a> Compiler<'a> { } } + self.register_counter = saved_register_counter; + self.current_package = saved_package; + self.current_module_index = saved_module_index; + } else { + // Default-only rule — no body definitions to compile. + // Ensure rule_num_registers is sized so finish() won't panic. + if let Some(&rule_index) = self.rule_index_map.get(rule_path) { + while self.rule_num_registers.len() <= rule_index as usize { + self.rule_num_registers.push(0); + } + + // Add the rule to the data tree so it is discoverable. + let rule_path_parts: Vec<&str> = rule_path.split('.').collect(); + if let Some((rule_name, package_parts)) = rule_path_parts.split_last() { + let package_path: Vec = + package_parts.iter().map(|s| s.to_string()).collect(); + + let _ = self.program.add_rule_to_tree( + &package_path, + rule_name, + rule_index as usize, + ); + } + } + self.register_counter = saved_register_counter; self.current_package = saved_package; self.current_module_index = saved_module_index; diff --git a/src/rvm/vm/rules.rs b/src/rvm/vm/rules.rs index f53828d..9858367 100644 --- a/src/rvm/vm/rules.rs +++ b/src/rvm/vm/rules.rs @@ -191,7 +191,13 @@ impl RegoVM { let rule_definitions = rule_info.definitions.clone(); if rule_definitions.is_empty() { - let result = Value::Undefined; + // No compiled definitions — check for a default value before returning Undefined. + // Default-only rules (e.g., `default deny := true`) have no body definitions + // but their default value was evaluated at compile time and stored as a literal. + let result = rule_info + .default_literal_index + .and_then(|idx| self.program.literals.get(usize::from(idx)).cloned()) + .unwrap_or(Value::Undefined); if !is_function_rule { let available = self.rule_cache.len(); let entry = @@ -336,7 +342,11 @@ impl RegoVM { } if rule_info.definitions.is_empty() { - let result = Value::Undefined; + // No compiled definitions — check for a default value before returning Undefined. + let result = rule_info + .default_literal_index + .and_then(|idx| self.program.literals.get(usize::from(idx)).cloned()) + .unwrap_or(Value::Undefined); if !is_function_rule { let available = self.rule_cache.len(); let entry = diff --git a/tests/interpreter/cases/default/basic.yaml b/tests/interpreter/cases/default/basic.yaml index c864292..1ea298f 100644 --- a/tests/interpreter/cases/default/basic.yaml +++ b/tests/interpreter/cases/default/basic.yaml @@ -321,6 +321,37 @@ cases: skip: true want_result: [5] + - note: default_only_rule_bool + data: {} + modules: + - | + package test + default deny := true + query: data.test.deny + want_result: true + + - note: default_only_rule_object + data: {} + modules: + - | + package test + default deny := {"result": false, "reasons": []} + query: data.test.deny + want_result: + result: false + reasons: [] + + - note: default_only_rule_with_package_query + data: {} + modules: + - | + package graph.mypolicy + default deny := {"result": false, "reasons": []} + query: data.graph.mypolicy.deny + want_result: + result: false + reasons: [] + - note: valid-var-in-some-in-value data: {} modules: diff --git a/tests/rvm/rego/cases/default_rules.yaml b/tests/rvm/rego/cases/default_rules.yaml index d7a785c..2aee74b 100644 --- a/tests/rvm/rego/cases/default_rules.yaml +++ b/tests/rvm/rego/cases/default_rules.yaml @@ -154,6 +154,39 @@ cases: query: data.test.auth.allow want_result: false + - note: default_only_rule_bool + data: {} + modules: + - | + package test + default deny := true + query: data.test.deny + want_result: true + + - note: default_only_rule_object + data: {} + modules: + - | + package test + default deny := {"result": false, "reasons": []} + query: data.test.deny + want_result: + result: false + reasons: [] + + - note: default_only_rule_with_entry_point + data: {} + modules: + - | + package graph.mypolicy + default deny := {"result": false, "reasons": []} + entry_points: + - data.graph.mypolicy.deny + query: data.graph.mypolicy.deny + want_result: + result: false + reasons: [] + - note: multiple_default_rules_different_names skip: true data: {}