skip to content

Rules Over Real Input

Real policies walk a nested manifest, choose between a set of violation messages and a boolean allow, and pull facts from somewhere. Interviewers probe it because the shape decides how a rule fails.

on this pageshow

explore

questions

19

In Rego, what does a deny rule written as a partial set of messages produce when no input violates it?

level: juniorimportance: must knowfreq 68%

answer

  1. not a boolean
  2. one element per violation
  3. the rule is always defined
  4. clean input yields the empty set
  5. conftest looks the name up

basics

~10 s

It produces the empty set. Each successful evaluation of the rule body adds one message; when nothing matches, there are zero messages, and a runner such as conftest reports that as a pass.

solid answer

~40 s

`deny` written as a partial set is not a boolean. Every time the rule body succeeds for some binding of its variables, the message it builds is added to a set, so one run over a CI job definition can return several messages at once — one per job whose build-log retention is below the required floor. When no body evaluation succeeds, the rule still has a value: the empty set. A partial rule is always defined, so you never get an undefined error for a clean file. conftest evaluates the rules it finds by name in a namespace — `deny`, `violation` and `warn`, optionally with a trailing suffix such as `deny_retention` — and exits non-zero if any `deny` or `violation` message came back. Empty set, no messages, exit zero, gate green.

code

rego · 12 lines
rego
package ci.retention

min_days := 90

deny contains msg if {
	some job_name, job in input.jobs
	job.artifacts.retention_days < min_days
	msg := sprintf(
		"job %q keeps build logs %d days; policy requires %d",
		[job_name, job.artifacts.retention_days, min_days],
	)
}

go deeper

for a junior

Be ready to state that deny is a set of strings, that each match adds one message, and that a clean input leaves the set empty rather than undefined or false.

for a middle

Explain the mechanics: partial versus complete rules, why every variable binding is tried, and how conftest turns a non-empty deny set into a non-zero exit code.

for a senior

Show that you know an empty set is ambiguous in production — it means either compliance or that nothing evaluated — and say how you would tell the two apart.

for a principal

Own the convention itself: the ecosystem's default shape optimises for cheap contribution and readable messages, and you should be able to say what that costs in failure direction.

## Two rule shapes in Rego Rego rules come in two shapes, and the difference decides how a policy behaves when nothing happens. A **complete rule** produces a single value: `allow := true`, `max_age := 90`. If its body does not succeed, the rule is *undefined* — it has no value at all, and a query for it returns nothing. A **partial rule** produces a collection built up from every successful evaluation of its body. `deny` is conventionally written as a partial **set** of strings — one string per thing that is wrong. ```rego package ci.retention min_days := 90 deny contains msg if { some job_name, job in input.jobs job.artifacts.retention_days < min_days msg := sprintf("job %q keeps build logs %d days; policy requires %d", [job_name, job.artifacts.retention_days, min_days]) } ``` Read that as a loop rather than as an `if` statement. The engine tries every binding of `job_name`/`job` over `input.jobs`. For each binding where the comparison holds, `msg` is computed and added to the set. A pipeline file with three under-retained jobs yields three messages in one run — the rule does not stop at the first failure, which is why a developer sees the whole list instead of fixing violations one at a time. ## The value when nothing matches This is the point of the question. If no binding satisfies the body, `deny` is **not** undefined and **not** `false`. It is the **empty set**. A partial rule is always defined; the only question is how many elements it has. That matters because the whole convention rests on it: *no messages means nothing to report*, which the surrounding runner interprets as a pass. There is no separate "the policy approved this" signal — approval is the absence of complaint. ## How conftest turns that into an exit code conftest does not ask the policy a general question. It parses the input document (YAML, JSON, HCL, Dockerfile and others), then looks up rules **by name** inside a namespace, which defaults to `main` and can be selected with `--namespace` or widened with `--all-namespaces`. The names it recognises are `deny`, `violation` and `warn`, each of which may carry a trailing suffix — `deny_retention`, `warn_missing_owner` — so several independently named rules can coexist in one package. - messages from `deny` and `violation` are reported as **failures** and make the process exit non-zero; - messages from `warn` are reported as **warnings** and do not fail the run by default. So the contract a policy author is writing against is: *put a string in the `deny` set and the build fails; put nothing there and it passes.* ## Building the message Because the element of the set is whatever you put there, the message is the entire output the developer gets. `sprintf` with the offending value interpolated (`sprintf("...%d...", [days])`) is the normal way to build it. A message of `"policy violation"` technically satisfies the shape and is useless in practice. A `violation` rule can produce a structured object rather than a plain string, which is what tooling reaches for when something downstream needs to parse the result rather than print it. ## The obvious trap Since the empty set is what a passing file produces, an empty set is *also* what you get when your rule never ran — the package was renamed, the namespace was not selected, the file was not in the policy directory. Both cases look identical from the outside: zero failures, exit code zero, green check. Nothing inside a deny-set policy distinguishes "I evaluated this and it is fine" from "I evaluated nothing". That asymmetry is the reason the alternative shape — a complete `allow` rule with `default allow := false` — exists, and the reason mature policy repositories keep a deliberately non-compliant fixture that the gate is asserted to reject. ## What to say in an interview Name the shape (partial set), say that each successful body evaluation contributes one element, say the value on a clean input is the empty set rather than undefined or false, and connect it to the runner: conftest looks up `deny`/`warn` by name and turns a non-empty `deny` set into a non-zero exit code.

  • Does the rule stop at the first violation it finds?
    No. The body is evaluated for every binding of its variables, and each success contributes one element to the set. A file with five offending jobs produces five messages in a single run, so the developer sees the whole list instead of fixing one violation per pipeline attempt.
  • What is the difference between a deny message and a warn message to conftest?
    conftest reports `deny` and `violation` messages as failures and exits non-zero when any are present; `warn` messages are printed as warnings and do not fail the run by default. The rule body and the message you build are otherwise written exactly the same way.
  • If deny is a set, can the same message appear twice in one run?
    No — a set deduplicates. If two different jobs produce byte-identical message strings, only one element survives and the report shows a single line. Interpolating the offending job name or field path into the message keeps distinct violations distinct.

It is a complaints box, not a verdict. The box is emptied at the end of the run; an empty box is taken to mean everyone was happy, even if nobody was ever asked.

saying these in an interview costs you the question

  • Says a deny rule returns true or false
  • Claims deny is undefined when nothing matches
  • Thinks evaluation stops at the first violation
  • Reads a zero-failure report as proof the rule ran
  • Writes a constant message with no offending value in it

context

open as a page

In Rego, where does a rule get a fact that the artifact under evaluation does not contain?

level: juniorimportance: must knowfreq 78%

basics

~20 s

A Rego rule cannot invent a fact. It must be loaded into the engine before the query, supplied by the caller inside the query itself, or fetched during evaluation with http.send. Nothing else reaches a rule.

open as a page

Why does a Rego rule written as deny[msg] { ... } fail to parse under OPA v1?

level: juniorimportance: must knowfreq 74%

basics

~20 s

OPA v1 makes if and contains mandatory, so a partial set rule must read deny contains msg if { ... }. The bare v0 head is a parse error, not a deprecation. import rego.v1 opts a single file into v1 syntax on an older engine.

open as a page

Why does a Rego policy shaped as a deny set fail open while default allow := false fails closed?

level: middleimportance: must knowfreq 57%

basics

~20 s

A deny set treats absence of messages as approval, so anything that stops the rule from matching produces a pass. A complete allow rule with default false has an explicit value of false whenever its body does not succeed, so absence of evidence becomes a denial.

open as a page

In Rego, when does object.get beat a direct lookup of a field that may be missing?

level: middleimportance: must knowfreq 68%

basics

~20 s

Use object.get whenever the field is optional and the rule must still reach a verdict. A direct reference to a missing key is undefined, so the whole rule body stops and yields nothing; object.get substitutes your default and evaluation continues.

open as a page

Your Rego deny rule stopped firing after you moved its check into a helper — why?

level: seniorimportance: must knowfreq 53%

basics

~20 s

The helper is undefined for that input, and an undefined call makes the whole rule body undefined. A partial deny rule with an undefined body adds no message, so the image passes. Undefined is not false.

open as a page

In Rego, when do you write a function with arguments instead of a helper rule?

level: juniorimportance: should knowfreq 58%

basics

~20 s

Use a function when one check must run over several different values: it takes arguments and evaluates per call site. Use a helper rule when the value depends only on the input, so OPA computes it once.

open as a page

In Rego, what does the walk built-in produce as it traverses a nested document?

level: juniorimportance: should knowfreq 55%

basics

~20 s

walk emits one [path, value] pair for every node in the document, starting with the root itself. The path is an array of object keys and array indexes; the value is the whole subtree sitting at that path.

open as a page

What do the timeout, raise_error and force_cache options on Rego's http.send do?

level: middleimportance: should knowfreq 55%

basics

~20 s

In Rego's http.send, timeout bounds how long evaluation waits. raise_error decides whether a failed call aborts evaluation or returns an error field. force_cache reuses a response across queries for force_cache_duration_seconds, ignoring the server's cache headers.

open as a page

How do you unit-test a Rego rule that calls http.send or reads from data?

level: middleimportance: should knowfreq 48%

basics

~20 s

Use the with keyword to replace evaluation context: with input as {...} supplies a fake document, with data.x as {...} replaces a data subtree, and with http.send as {...} mocks the built-in, so opa test runs offline and deterministically.

open as a page

In Rego, what happens when one function name has two definitions that disagree?

level: middleimportance: should knowfreq 41%

basics

~20 s

Rego allows a name to be defined twice, but a function must not produce two different values for the same arguments. When both definitions hold and disagree, OPA fails the query with a conflict error.

open as a page

Your Rego deny rule skips its message for waived repositories — what does that silence cost you?

level: seniorimportance: should knowfreq 39%

basics

~20 s

A waived repository then produces output identical to a compliant one: no messages, exit zero. You lose the ability to tell an exception from a pass, to count how many waivers are live, and to notice when the rule stopped firing at all.

open as a page

OPA allowed an unencrypted volume though a Rego rule forbids it — how do you check the rule was loaded?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Separate evaluated-and-found-nothing from never-loaded. Ask the running engine which modules it holds and whether its bundle activated, then replay the exact input against that same bundle. A bundle that fails to compile is rejected whole, leaving older policy serving.

open as a page

In Rego, how would you warn on every removed apiVersion anywhere in a repo's manifest bundle?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Walk the whole bundle instead of enumerating paths: for every node that is an object whose apiVersion is in your removed set, emit a warning that includes the walk path. The path is what makes the advisory list actionable.

open as a page

What does opa check --strict reject in a .rego file that plain opa check accepts?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Strict mode promotes lint-grade problems to compile errors: duplicate imports, unused imports, unused local assignments, using input or data as a variable or rule name, and deprecated built-in aliases. Plain opa check only reports genuine parse and compile errors.

open as a page

In Rego, how do you assert on a JSON blob that arrives as a string field?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Decode it first: json.unmarshal turns the string into a value you can reference, and yaml.unmarshal does the same for embedded YAML. Until you decode, traversal sees one opaque string and finds none of the fields inside it.

open as a page

An auditor asks which licence list your Rego policy enforced six months ago. How do you answer?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

You can answer only if the fact was pinned. A licence list shipped in the policy's data document has a version you can name and replay; one fetched by http.send at decision time was never captured.

open as a page

Why does OPA reject a Rego helper that calls itself to walk a base-image chain?

level: seniorimportance: nice to knowfreq 29%

basics

~10 s

Rego forbids recursion. The compiler builds a dependency graph over rules and functions and rejects any cycle, so the helper never loads. Use a transitive-closure built-in, or resolve the chain before evaluation.

open as a page

You own a shared Rego policy library for 40 teams — standardize on deny sets or one allow decision?

level: principalimportance: nice to knowfreq 27%

basics

~20 s

Standardize on deny sets for almost everything, because they are additive, tool-native and produce usable messages, then buy back the fail-closed property in the harness. Reserve an explicit allow decision for the few gates where a silently absent rule is unacceptable.

open as a page