In Rego, what does a deny rule written as a partial set of messages produce when no input violates it?
answer
- not a boolean
- one element per violation
- the rule is always defined
- clean input yields the empty set
- conftest looks the name up
basics
~10 sIt 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 linespackage 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
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.
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.
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.
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