Skip to content

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/.

version: 1
organisation: Example Operator Ltd
release: fixreg@2025.06 # the pin: which corpus these atom ids are from
controls:
- 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.02

An 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.

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_against is 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.

When the check fails, or when you take a release for the first time:

  1. openregs diff <reviewed>..<pinned> — what moved, with the citation, and which release moved it.
  2. Read the obligation as the pinned release states it, and decide: does the control still discharge it? Does the rationale still hold?
  3. Change the control, or the waiver, if the answer is no.
  4. 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.

Rendered from openregs/openregs@f3a2d10:docs/control-mappings.md