Control mappings — controls.yaml, and keeping one true
A control mapping is the file in which an organisation records which of its
own internal controls discharge which OpenRegs obligation atoms. It is the input
openregs validate controls, openregs impact, openregs coverage and
openregs bump all read.
It lives in your repository, never in this one. Nothing about it is scoped by industry and OpenRegs privileges no control framework: a control identifier is opaque here, it addresses a record in whatever system you keep, and the schema never asks which framework it came from.
The shape is spec/schemas/controls.schema.json;
the worked examples are in fixtures/controls/ and a
whole consuming repository, workflow included, is in
fixtures/consumers/.
An entry says one of two things
Section titled “An entry says one of two things”version: 1organisation: Example Operator Ltdrelease: fixreg@2025.06 # the pin: which corpus these atom ids are fromcontrols: - atom: FIXREG-Art5.3-Ob1 # a mapping: a control covers this duty control: CTRL-INC-014 owner: incident-response@example.org evidence: https://grc.example.org/controls/CTRL-INC-014 reviewed_against: fixreg@2025.04
- atom: FIXREG-Art5.2-Ob1 # a waiver: it does not apply, and here is why status: not_applicable rationale: >- No critical service is provided from this entity; the register is maintained centrally under the arrangement recorded in GRC-2291. reviewed_against: fixreg@2025.02An atom with no entry is neither: it is an unmapped gap, and
openregs coverage is the report that names it. An atom entered twice is
refused — “covered by CTRL-INC-014” and “not applicable to us” cannot both be
true.
The pin and the review are two different facts
Section titled “The pin and the review are two different facts”release: is the pin: which corpus the atom ids in this file come from.
openregs bump rewrites that one line when you take a new release, and — by
design — touches nothing else. Every mapping and every waiver comes through a
bump exactly as it was written.
That is correct, and it is also why the pin cannot tell you whether an entry
still stands. An atom id is permanent; the duty behind it is not. FIXREG’s
standing scenario is the case in point: FIXREG-Art5.3-Ob1 keeps its id across
the 2025.04 amendment while the incident-reporting threshold it turns on goes
from EUR 5000 to EUR 10000. An entry written against the old words is not wrong
in any way a schema can see, and a bumped pin makes it look freshly considered.
reviewed_against: is the second fact: the release whose words your reviewer
actually read. It is per entry, it does not move when the pin moves, and it is
what makes staleness visible in the file itself rather than only in the CI run
that happened at the moment of the bump.
What the check does
Section titled “What the check does”openregs validate controls <file> walks the releases from each entry’s
reviewed_against up to the pinned one, reading the diff manifest each of those
releases shipped, and fails the entry if any of them changed that atom
substantively:
FAIL controls.yaml: 1 controls violation controls.0.reviewed_against: FIXREG-Art5.3-Ob1 changed substantively in fixreg@2025.04, after the fixreg@2025.02 this entry records being reviewed against (a control is recorded as discharging a duty whose meaning has moved since anybody read it; re-read the obligation as fixreg@2025.06 states it and confirm the control still covers it, then record that review by setting reviewed_against: fixreg@2025.06. `openregs diff fixreg@2025.02..fixreg@2025.06` prints what moved, with the citation and the release that moved it)Three things it deliberately does not do:
- An editorial change is not staleness. The release that changed the atom said the record moved and the law did not, so a review taken before it still stands. The classification is the release’s own, read from the manifest and never recomputed here: whether an amendment was editorial rests on the amending act’s recitals, which your checkout does not contain.
- It infers nothing from the pin. An entry with no
reviewed_againstis neither current nor stale — no review is on record, and none is invented. A mapping written before you started recording reviews still says exactly what it always said. - It does not care how the file got there. Bumped one release at a time, bumped across several at once, or hand-edited: the property is checked at rest, so it holds on every later run and not only on the pull request that moved the pin.
A waiver is checked the same way as a mapping. A rationale reasons about an
obligation, and it can stop being true when that obligation changes — which is
exactly what the rationale in
fixtures/controls/waived.yaml says about
itself when it asks to be reassessed if the threshold moves.
Recording a review
Section titled “Recording a review”When the check fails, or when you take a release for the first time:
openregs diff <reviewed>..<pinned>— what moved, with the citation, and which release moved it.- Read the obligation as the pinned release states it, and decide: does the control still discharge it? Does the rationale still hold?
- Change the control, or the waiver, if the answer is no.
- Set
reviewed_against:on that entry to the pinned release — and only then. The field is a record of a review, so writing it without doing one is the one way to make the check worthless.
openregs impact --diff <old>..<new> --map controls.yaml remains the report for
a single release move: it says what a bump does to the controls you have, with
owners and citations, and fails the build when a mapped atom changed
substantively. The two are complements — impact is the moment of the move, the
review record is what is still true a year later.
Related
Section titled “Related”GLOSSARY.md—controls.yaml, obligation atom, diff manifest.integrations/github-actions.mdandintegrations/gitlab-ci.md— running the checks on the pull request that moves your pin.
Rendered from openregs/openregs@f3a2d10:docs/control-mappings.md