Repository navigation
core: decide on the words ergo policies and reports use #113
Description
When we started ergo, we picked words for things as we went: "policy", "requirement", "check", "subject" and so on. They worked well enough to get going, but nobody sat down and chose them on purpose. ergo is still early, so now is a good time to ask whether they're the right ones, before more people start writing policies with them.
This issue is a place to talk about that. Any thought is welcome, from "this word confuses me" to "the whole shape should be different".
The words we use today
Here's the example from the README:
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]In the policy:
- policy: the file that holds the requirements and asks ergo for a report
- requirement: one thing that must be true, like
prod_deploy - subject: one of the things a requirement is about, like a deployment, with
subject_type,fromandid - applies_to: the filters that pick which subjects the requirement covers
- check: one rule each subject must pass, like
approved - op: the operator a check or filter uses, like
equalsornon_empty_string
In the report:
- compliant: the overall yes or no
- satisfied: whether a requirement passed
- results: one row per subject and check
- cause: why a check failed, like
absentorvalue - violations: the failed rows that are real problems
- the checks ergo adds itself:
$well_formed,$min_subjectsand$applies
The requirements wrapper
Every policy starts with a requirements: key that holds a map of requirements. It doesn't tell the reader much, and it's there mostly because OPA turns the YAML file into data.requirements. Could we drop it, so the top-level keys are the requirements themselves? Or go further and have one requirement per file?
An idea from @tooky: start with what the policy means
Tooky suggested a policy that opens with the requirement in plain words and fills in the details after, so you can understand it before reading how it's checked:
prod_deploy:
description: Production deployments must be approved by someone
subject: deployment
where:
is_prod: { environment: { equals: prod } }
requirements:
approved:
description: Someone approved the deployment
approved_by: { is: present }
subjects:
deployment:
from: deployments
id: idCompared with today, this:
- gives the top-level requirement its own
description - calls the filters
whereinstead ofapplies_to - calls the checks
requirements, which reads more like what a control owner would write - moves
fromandidinto a separatesubjectssection, so several requirements can share one definition of "deployment" - writes conditions as
field: { operator: value }instead ofop,pathandvalue
Some questions it raises: how would we write a path into nested fields, or a field whose name is also an operator name? And if checks become "requirements", what do we call the top level?
Things worth asking
- Do these words mean the same thing to an engineer, a control owner and an auditor? Those are the people we want reading ergo policies and reports.
- Do any of them clash with words people already use in compliance, in OPA, or at Kosli? "Policy" means something in OPA already, and "control" or "evidence" might fit some ideas better.
- Is the shape right? For example, should a requirement hold its checks the way it does now, or should they be split differently?
- Are there words we're missing, or words we use for two different things?
If you'd rename something, say what you'd call it and why, ideally with an example of how a policy or report would read with your word.
ergo is still alpha, so breaking changes are fine if they make it clearer.
Activity
jbpros commentedon Oct 4, 2026
@mbevc1 @meekrosoft @pbeckham @jumboduck @JonJagger @ToreMerkely everyone's input is welcome here :) feel free to tag other people
jbpros commentedon Oct 4, 2026
Tooky's separate
subjectssection would be a good home for the subject's plural:The checks ergo adds need it. #116 asked for plain descriptions like "At least 2 deployments are required", but a plural guessed from the word's ending turns
personinto "persons", and English has too many exceptions to list (child,index,criterion,analysis...). So #119 says "The deployment count is at least 2" for now, which reads right for any word. With apluralon the subject, every requirement using it could say "At least 2 people are required" instead.pluralwould be optional, with a default for common English words: an-s,-x,-z,-chor-shending takes-es, a consonant before-ybecomes-ies, and anything else takes-s. That coversdeployment,batch,repositoryandpull request. The author only writespluralfor an irregular word, and the default rules are short enough for any implementation to copy.jbpros commentedon Oct 4, 2026
If subjects get their own section, a subject's name can't be empty or only whitespace, the same as
subject_typetoday (#121). JSON and YAML both allow""as a key, so the check on thesubjectssection needs that rule, next to "every requirement'ssubjectexists".core: describe the checks ergo adds in plain words (#119)
core: fail $well_formed when subject_type is empty or only whitespace (…
ToreMerkely commentedon Oct 5, 2026
Changes we'd like in ergo
Several of these are already possible in a Rego policy, by building the requirements or the document in Rego. They are gaps for policies written as YAML or JSON data.
Words and shape
id,titleanddescriptionat the top level, with the requirement's fields beside them. Keeprequirements:only for a control with more than one.DEV-0501, segregation of duties: a change must not be deployed by the person who made it.
Today the control text only fits in a comment, and a control with one requirement still needs the wrapper and a name for the requirement:
With the control at the top level, the text is part of the policy and can appear in the report:
DEV-0401: security testing, code review and vulnerability scanning are done, and vulnerabilities are fixed in time.
A control that needs several requirements keeps them under
requirements::A Rego policy can put these in OPA's
# METADATAannotations. See also core: decide how policies and reports carry extra data, like notes on checks and labels on inputs #114.subjectssection that can hold filters.DEV-0502: new features need approval before they reach production.
DEV-0403: testing must be finished before production.
Today DEV-0102, DEV-0504, DEV-0502 and DEV-0703 all repeat the same subject and filters:
Defined once, each control names it:
DEV-0405: a data migration must be shown to be complete and correct.
Keys that contain a dot, like
app.kubernetes.io/name, need an escape, for examplemetadata.labels."app.kubernetes.io/name".Operators
within: {days: N}, with N from params or a lookup by severity.DEV-0401: vulnerabilities must be fixed within a deadline set by their severity.
DEV-0705: security tests must run at regular intervals.
Today a Rego wrapper computes a deadline for each subject, so a YAML policy can't express it:
With the operator, the YAML holds the rule, and the number of days can depend on a field of the subject:
not_inoperator.DEV-0406: acceptance testing is done by someone outside the development team.
Today it takes
allandcompare:which the report renders as
every $$input.artifact.developers: $$input.artifact.developers[] ne $$input.artifact.attestations.uat.tested_by. Withnot_in:which could render as
tested_by not in $$input.artifact.developers.Scope and reuse
requirements.DEV-0403: new features must be tested.
Today each test run is filtered on the deployment's type, and
min_subjects: 0keeps a normal change from failing. As a side effect, an emptyrequired_testsparam also passes:A requirement-level condition would skip the whole requirement, report it as not applicable, and keep
min_subjectsat 1:suite_requirement(name)called once per suite.DEV-0502: the business owner approves, with sign-off from QA and development.
Today the check is written three times, once per role. With a list of values:
Each role would still get its own row in the report.
DEV-0103 and DEV-0102: the same impact analysis rule, once for normal changes and once for new features.
Today DEV-0103 is a copy of DEV-0102 with
new_developmentreplaced bynormal. With a parameterized requirement:DEV-0406: acceptance testing is documented and approved before production.
Today
each_ason theuatobject makes each of its fields a subject, so a nested check has to reach the tester through$$input:With a name for the single object:
Report
all/any(core: show which item failed in the rows of all and any checks #68).DEV-0501: a change must not be deployed by the person who made it.
Today the violation lists every commit, and the reader has to find bob's:
With the failing items named:
$appliesfailures out ofviolations, or label them as a scope problem.DEV-0405: applies to new features and to changes that migrate data.
Today a deployment that doesn't say whether it migrates data appears next to the real failures:
{ "subject": { "id": "dep-4" }, "check": "$applies", "cause": "absent" }It still has to fail the control, but under its own heading:
jbpros commentedon Oct 5, 2026
@ToreMerkely I went through each request against
main. Three of them work today, two I'd leave for now, and the rest go to issues of their own so this one can stay about words and shape.Already works
not_infor DEV-0406.excludeswith a ref does it in one check:With
developers: [ann, bob]andtested_by: bob, it fails with:{ "check": "independent_tester", "expression": "not contains($$input.artifact.developers, $$input.artifact.attestations.uat.tested_by)", "inputs": [ { "name": "$$input.artifact.developers", "value": ["ann", "bob"] }, { "name": "$$input.artifact.attestations.uat.tested_by", "value": "bob" } ], "cause": "value" }That also removes the need to name the
uatobject for this control.DEV-0102 and DEV-0103 as one requirement. Read the change type from the params, and ship the same policy with two params files:
With deployments
d1(normal) andd2(new_development), both failing,change_type: new_developmentreportsd2andchange_type: normalreportsd1.$appliesfailures inviolations.violationsalready leaves out the subjects that are simply out of scope, the$appliesrows with causevalue. It only keeps the ones where the filter couldn't be read. For DEV-0405, withdep-1sayingincludes_data_migration: falseanddep-4not saying anything:dep-1isn't there.dep-4has to stay, because the control fails because of it, and without that row nothing would say which deployment is missing the field. A reporting tool can put these rows under their own heading already: theircheckis$appliesand their cause isn'tvalue.Not now
Dotted paths. Quoting keys the way the report does, like
metadata.labels."app.kubernetes.io/name", would work, but then every policy path is a string ergo has to parse, with its own rules for quotes, escapes inside quotes, and list indexes likexs.0against the keyxs."0". Every implementation would have to parse them exactly the same way, or the same policy would read different fields. The report only writes that form, it never reads it back. In YAML the list form is about as short, and there's nothing to parse:[migration_test, migrated_records]. So I'd hold off until the list form gets in someone's way.Reuse with
for_eachanduses/with. Six of your new-feature controls have a normal-change twin with the same rule (0102/0103, 0403/0404, 0406/0407, 0409/0410, 0502/0503, 0504/0505). Each pair is one policy withvalue: {ref: [$$params, change_type]}and two params files, as above. 0703 and 0704 differ in what they require, so they stay two policies anyway. The three DEV-0502 sign-offs give the same three rows as three checks written out. Sharing requirements between controls would mean one policy file including another, which ergo has nothing for. So I'd hold off until a control needs more than params.Sections next to
requirementsA shared
subjectssection, and maybe a section for the control's id, title and description, would sit next torequirementsin the same file:The file's top-level object is the policy, with no root element around it. An implementation in another language reads it as one object. In Rego, OPA loads each section into
datanext to everything else, and passingdatawhole doesn't compile, because the calling rule is part of it. Soergo.report(input, data.requirements)stays as it is, and ergo readsdata.subjectsitself, the way it already readsdata.params. A requirement whosesubjectisn't there fails$well_formed. A policy built in Rego would pass its sections explicitly, asergo.report_with_paramsdoes for params.As with
params, the policy that calls ergo then can't live in a package calledsubjects, or under one.Moved elsewhere
all/any: core: show which item failed in the rows of all and any checks #68, with DEV-0501 added as a case.id,titleanddescription: core: decide how policies and reports carry extra data, like notes on checks and labels on inputs #114, which asks where notes like a link to a control go.idbeside the requirement's fields would clash with the requirement's ownid, the path to the subject's id, so they'd need a section of their own (above).within: core: add a within operator that checks a time is at most N days before another #140when: core: skip a requirement, and report it as not applicable, when a condition on the input doesn't hold #141The
subjectssection itself stays here.jbpros commentedon Oct 5, 2026
A change to the sections part above: rather than have ergo read
data.subjectsitself, I'd makeergo.reporta pure function that gets everything it uses as arguments:policyholdsrequirements,subjectsand any section added later, and a section that isn't there counts as empty. This replacesergo.report_with_params.ergo.reportstops readingdata.params, which is the only thing it reads fromdatatoday, so the policy that calls it can live in any package,paramsincluded.The catch is Rego's: if an argument is undefined, the whole report is undefined, with no rows to say why. A file with no
subjects:, or a run with no params, would do that. So the policy that calls ergo gives each one a default:Each default still fails closed. With no params, every
$$paramsread fails asabsent. With nosubjects, a requirement that names one fails$well_formed. With norequirements, say a file that spells itrequirments:, the policy has nothing to check, and that's never compliant. This wrapper is the same for every YAML policy, so the README can show it once.It breaks every existing call.
ergo.report(input, requirements)becomesergo.report(input, {"requirements": requirements}, params).jbpros commentedon Oct 5, 2026
One more for the list: "satisfied" means two different things in a report.
satisfiedon a requirement istrueorfalse: whether the whole requirement is met."cause": "satisfied"on a row is what a passing check gets, next tovalue,absent,nulland the rest for failures.So a report can say both of these, about different levels:
{ "check": "$min_subjects", "passed": true, "cause": "satisfied" }People reading a report mix them up. The glossary above also says
causeis "why a check failed", but passing rows have one too. Maybe a passing row's cause should be called something else, or the requirement field should be renamed, for examplemet.🤖 Generated with Claude Code
core: list the items that failed an all or any check (#145)
core: let requirements and checks carry a description and meta (#151)
jbpros commentedon Oct 7, 2026
For the control text Tore asked for: since #151, a requirement can have a
description, and ametaobject for anything else, like a control id or title. Both are copied into the report. #114 has DEV-0501 written that way. A control with one requirement still needs therequirements:wrapper and a name for the requirement, and fields for the whole policy still wait on the shape decided here.🤖 Generated with Claude Code
mbevc1 commentedon Oct 7, 2026
Perhaps I'm thinking along the lines of @tooky 's suggestion, but
subject_typefeel awkward wording, but I don't have a better suggestion yet. I need to think more about this and try different scenarios 😄jbpros commentedon Oct 7, 2026
@mbevc1 I think we will drop subject_type altogether.
I am convinced about this new shape. Define subjects separately, refer to them in the requirements.
No more
subject_type🥳mbevc1 commentedon Oct 7, 2026
@jbpros makes sense and reads better now 🚀
core: define subjects once in the policy and name them from requirements
core: take the policy and params as arguments to ergo.report (#163)
core: define subjects once in the policy and name them from requirements
core: define subjects once in the policy and name them from requireme…
jbpros commentedon Oct 9, 2026
How a single check or filter is written has moved to #174: one-line sentences like
approved_by must not be emptyinside the YAML skeleton being settled here. Tooky'sfield: { operator: value }and Peter'sselector/must/containexamples are answered there. This issue stays about the words and the shape around the checks.spec: add requirements, scope, statuses, row order and violations
spec: add requirements, scope, statuses, row order and violations