Skip to main content

ADR 007: GRC control model — frameworks, requirements and controls

Status: Accepted

Context​

RiskFlow is growing from separate compliance tools (SRA, BAA, PnP, training) into a Vanta-style program: a clinic turns on a law such as the HIPAA Security Rule, sees what it asks for, and tracks the concrete things it does to meet it. Before any tables or endpoints exist, everyone building this (people and AI agents) needs the same picture of how those pieces fit together.

Two things make this harder than a checklist:

  • The law is vague on purpose. HIPAA 164.312(d) says "implement procedures to verify that a person or entity seeking access … is the one claimed". It does not say "turn on MFA". The same requirement is met in different ways by different clinics, and one requirement usually needs several concrete safeguards.
  • One safeguard covers many requirements, and later many laws. Enforcing MFA helps meet several HIPAA requirements today and SOC 2 criteria later. If each requirement carried its own copy of "MFA enforced", a clinic would track the same thing in several places and the copies would drift apart.

The catalog of laws and our recommendations is the same for every clinic. Each clinic's own answers (who owns a safeguard, whether it applies, how it is doing) are private to that clinic.

Decision​

1. The model​

Framework (HIPAA Security Rule) ← the law. Global, seeded by us.
└─ Requirement (164.312(d) "Person or entity authentication")
▲ many-to-many
ControlTemplate ("MFA enforced") ← our recommended way to meet it. Global, seeded by us.
│ copied when a team turns on a framework
Control (team's own "MFA enforced") ← owned by one team: owner, status, N/A reason.
  • A Framework is a law or standard (hipaa-security). It has a version so a future rule change can live next to the old one.
  • A Requirement is one thing the framework asks for, keyed by the citation people recognize (164.312(d)). Requirements form a tree: HIPAA has standards with implementation specifications underneath. Each one may be marked Required or Addressable.
  • A ControlTemplate is our recommended, concrete, checkable safeguard (mfa-enforced). Templates link to requirements many-to-many through ControlTemplateRequirement.
  • A Control is a team's own copy of a safeguard. It carries the team's owner, status, and not-applicable reason. ControlRequirement records which requirements that team's control covers.

2. Requirements and controls are separate​

We do not attach controls to requirements as children, and we do not treat a requirement as something a team "does". Requirements say what, controls say how, and the link between them is many-to-many. This lets one control (MFA) count toward several requirements and, later, several frameworks, so a clinic fixes it once and every requirement it covers moves together.

3. Catalog rows and team rows are split​

TablesteamIdWritten by
CatalogFramework, Requirement, ControlTemplate, ControlTemplateRequirementnoneidempotent seed scripts only
TeamTeamFramework, Control, ControlRequirementalwaysthe team, through the API
  • Catalog rows are read by every team and never written by request handlers. There is no per-team edit of a requirement or template.
  • Team rows follow the normal multi-tenancy rules: every query filters by teamId taken from the principal, never the body, and a miss across teams returns 404 (ADR 004).
  • ControlRequirement stores teamId itself (copied from Control) so tenant-scoped queries do not need a join.

4. Turning on a framework copies, it does not reference​

When a team turns on a framework (TeamFramework), we create a Control for each template linked to that framework's requirements and copy the template's requirement links into ControlRequirement. After that the team's controls are independent of the template:

  • teams can rename a control, write custom controls with no template (controlTemplateId = null), and change which requirements a control covers;
  • a team adopts a given template at most once (@@unique([teamId, controlTemplateId])), so a template shared by two frameworks becomes one control covering both;
  • later edits to the catalog do not silently change a team's controls. Pulling catalog updates into existing teams is a separate, explicit step.

5. Status is computed by pure rules from signals​

A control's status (NOT_STARTED, PASSING, FAILING, NEEDS_EVIDENCE, NOT_APPLICABLE) is the output of pure functions with no database or Nest dependency. Their input is a list of signals: small facts such as "this Google Workspace check failed" or "this document is missing". Requirement status and the framework's readiness score are computed from control statuses by the same kind of pure functions.

  • Status is stored on Control (with statusChangedAt) for fast dashboards, and recalculated by ControlStatusService whenever its inputs change. The stored value is a cache of the rules, not a field users edit.
  • Signals come from pluggable signal sources. Later epics add sources (Google tests, evidence, training, BAAs) without changing the rules.
  • Marking a control not applicable requires a written justification and records who approved it and when, because auditors read these reasons.
  • The readiness score counts only leaf requirements. A standard that has implementation specifications underneath is not scored itself, so nothing is counted twice.

6. Later​

Tests and their results, evidence, the risk register, assets, issues and the yearly SRA snapshot are later epics. They plug into this model as new signal sources and as tables that link to Control; they do not change the catalog and team split above.

Consequences​

  • Until a signal source exists, every control shows Not started and every framework scores 0%. That is expected at the end of the foundation epic.
  • Two link tables (ControlTemplateRequirement and ControlRequirement) hold similar data. This is deliberate: one is our recommendation, the other is the team's actual mapping, and they are allowed to differ.
  • Catalog fixes reach new teams automatically but not existing ones. A sync step for existing teams is needed before we ship a second version of a framework or change template links in a way teams should see.
  • Seeds must be idempotent and keyed on stable key values (hipaa-security, 164.312(d), mfa-enforced), never on autoincrement ids.
  • Adding a framework that reuses existing templates (for example SOC 2) adds requirements and template links only. Teams that already have those controls get the new coverage through ControlRequirement rows, not duplicate controls.

References​

  • Epic: GitHub issue #21 (GRC foundation)
  • apps/backend/prisma/schema/grc.prisma (issues #23, #24)
  • apps/backend/src/modules/controls/status/control-status.rules.ts (issue #27)
  • ADR 004 — 404 over 403
  • Glossary: Framework, Requirement, Control, Control template, Control status, Readiness score in CONTEXT.md