Explainable controls that run on Open Policy Agent and Rego.
ergo is an open source language for defining explainable controls. Instead of a binary result, every decision returns an audit report of what was checked, and why it passed or failed.
A decision nobody can inspect
isn't a control.
With Rego you can block a deployment, but you can't show later why. When you define controls with ergo you have records of why decisions were made.
In regulated settings, someone else has to be able to check that a control behaved as it should. A yes or no can't be validated. You don't just tell the auditor the decision. You hand over the workings.
Rego provides decisions
{
"allow": false
}Useful. But why was it false?
With ergo you get explanations
subject check value read passed cause
d-1 approved approved_by = "alice" true satisfied
d-2 approved approved_by = null false absent
d-3 $applies environment = "staging" false value (out of scope, recorded)Same decision. Now you can see why.
Write the control.
Not the sorcery.
# policy/requirements.yaml
requirements:
prod_deploy:
subject_type: deployment
from: [deployments]
id: [id]
applies_to:
is_prod:
op: equals
path: [environment]
value: prod
checks:
approved:
description: Someone approved the deployment
op: non_empty_string
path: [approved_by]# policy/deploy.rego
package deploy
import data.ergo
report := ergo.report(input, data.requirements)# policy/deploy.rego
package deploy
import data.ergo
requirements := {"prod_deploy": {
"subject_type": "deployment",
"from": ["deployments"],
"id": ["id"],
"applies_to": {"is_prod": {
"op": "equals",
"path": ["environment"],
"value": "prod",
}},
"checks": {"approved": {
"description": "Someone approved the deployment",
"op": "non_empty_string",
"path": ["approved_by"],
}},
}}
report := ergo.report(input, requirements)Defined in YAML or Rego
Write the requirement as a YAML file or as a Rego object. ergo reads both the same way and gives the same report.
- 1
Define the subjects
subject_type,fromandidsay what the subjects are and where to find them. - 2
Filter the scope
applies_tokeeps only the subjects this control is about. The rest are recorded, not dropped. - 3
Test the values
checksare the rules each subject must pass. ergo does the looping, the evaluation and the report.
One report shape.
Every control.
A deployment control. A code-review control. A vulnerability control. Different policy, same evidence: one row per subject and check. Passing and failing rows have the same fields.
- requirement
Which control produced the row.
- subject
The thing that was judged, with its type and id.
- check
The rule that ran.
- inputs
The values the check actually read.
- passed
The result. Always
trueorfalse. - cause
Why it reached that result.
Same policy + same input = the same report, byte for byte. Hash it. Diff it. Store it.
Failure isn't one state.
A missing field, a field set to null and a selector that matched nothing all read as null. They're different problems with different fixes. The cause tells them apart.
satisfiedThe check passed.
substitutedThe check failed, but its substitute passed.
ambiguousA selector matched more than one item.
unmatchedA selector matched nothing, although the list was there.
absentA field the check reads isn't there.
nullThe field is there, but
null.valueEverything was read fine. The values just don't pass.
approved_by = null
cause: absentThe evidence was never recorded.
approved_by = ""
cause: valueThe evidence exists, and it says no.
Different problem. Different owner. Different fix.
- Nothing disappears.
- Nothing is implied.
- Nothing is decorative.
- Everything has a reason.
- Everything has a state.
- ergo.
You didn't write the checks that start with $. ergo adds them, so that whenever a requirement isn't met, at least one row explains why.
- $well_formed
- The requirement itself makes sense: it has at least one check and a valid
require. - $min_subjects
- At least one subject was found. A typo in
fromfails, instead of passing with nothing checked. - $applies
- Whether each subject was in scope. Out-of-scope subjects stay in the report, with the reason.
Rego underneath.
ergo on top.
ergo doesn't replace OPA. It's a single Rego file you copy into your policies. The control is expressed as data, and ergo provides one evaluation and reporting model for all of them.
- inputdeployments.json
- controlrequirements
- libraryergo.rego
- engineOPA 1.19
- evidencereport
- compliant
Readable by machines.
Readable by people.
The vocabulary is small on purpose. Every check gets a plain-language expression, like approved_by is a non-empty string, rendered from the requirement itself. Controls are data, so they can be validated, diffed and tested like code.
One file. No lock-in.
- 01
Copy the library
Copy
ergo.regonext to your policies. That's the install.policy/ ergo.rego - 02
Write a requirement
Say what you're judging, what's in scope and what must hold.
report := ergo.report(input, data.requirements) - 03
Read the report
Ask OPA for the report, or just the violations.
opa eval -d policy -i deployments.json \ 'data.deploy.report'
ergo /ˈɜː.ɡəʊ/
Therefore. As a result. A conclusion that comes with its premises.
Not computer says no. Here is the decision, and here is what produced it.