Assurance Control Register v1
Contract ID: CONTRACT-ASSURANCE-CONTROL-REGISTER-V1-002 · Schema: assurance-control-register-v1 · Status: Active (NIS2 adapter implemented; other frameworks proposed) · Owner: Policy Guild
Implemented in src/Policy: StellaOps.Policy/Compliance/AssuranceControlRegisterModels.cs (AssuranceControlRegisterSchema, AssuranceControlMapping, AssuranceControlRegisterSnapshot), and surfaced by StellaOps.Policy.Engine/Endpoints/Nis2ControlRegisterEndpoints.cs.
Purpose
assurance-control-register-v1 is the generic control-to-evidence mapping shape for StellaOps Assurance packs. NIS2 is the first concrete implementation, but the contract is designed to also carry future frameworks such as ISO/IEC 27001, SOC 2, IEC 62443, or customer-specific assurance packs.
Audience: Policy Engine implementers and compliance consumers building or reading control registers. The framework-specific NIS2 contract remains active alongside this generic one: see nis2-control-register-v1.
Required Fields
These fields describe the canonical AssuranceControlRegisterSnapshot record (src/Policy/__Libraries/StellaOps.Policy/Compliance/AssuranceControlRegisterModels.cs).
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion | string | yes | MUST be assurance-control-register-v1. |
frameworkId | string | yes | Framework id such as nis2, iso27001, or soc2. Normalized to lower case. |
packId | string | yes | Pack id from assurance-pack-v1. Normalized to lower case. |
tenantId | string | yes | Tenant that owns the register. |
stellaRelease | string | yes | Stella release identifier the snapshot was produced under. |
generatedAt | RFC3339 timestamp | yes | UTC generation timestamp. |
controls[] | array | yes | Framework controls sorted deterministically. |
sourceRefs | object | no | Source policy, pack, and evidence refs (policyDocRef, packRef, evidenceBundleRef). |
The HTTP read responses (see Compatibility Mapping) add asOfDate, source, sourceOfTruth, compact items[], cursor, total, and a snapshotHash field (currently always null, reserved for when a stored snapshot is returned). snapshotHash is NOT part of the canonical AssuranceControlRegisterSnapshot record itself.
Control Shape
Reflects the AssuranceControlMapping record (AssuranceControlRegisterModels.cs).
| Field | Type | Required | Notes |
|---|---|---|---|
controlId | string | yes | Stable tenant-visible id. |
areaOrDomain | object | yes | Framework-specific grouping, for example NIS2 thematic area 1-13. |
externalRefs[] | string array | yes | Framework article/control/annex refs (for NIS2, derived from art21Refs[]). |
policyDocRef | string | yes | Human-readable policy or module doc reference. |
responsibleRole | string | yes | Role accountable for review and evidence freshness. |
applicability | string | yes | applicable, not-applicable, partially-applicable, or operator-supplied. |
applicabilityReason | string | conditional | Required unless applicability = applicable (the NIS2 adapter substitutes not specified when missing for non-applicable rows). |
implementingControls[] | array | yes | References to implementing controls (kind, ref, optional description, metadata). For NIS2 the binder accepts kinds gate, scope, metric, runbook. |
evidenceRefs[] | array | yes | Evidence refs with source labels (see Evidence Reference Shape). |
tenantEvidenceMode | string | yes | One of the four evidence source types (stella-native, tenant-supplied, mixed, external-reference); the NIS2 adapter projects the row’s evidenceMode. |
reviewCadence | object | yes | lastReviewed (date), nextReview (date), cadenceLabel (string; defaults to unspecified). |
metadata | object | no | String-normalized extension bag. |
Note: an
evidenceScopes[]per-control field was specified in an earlier draft but is NOT present in the implementedAssuranceControlMappingrecord and is NOT emitted by the read API. Evidence-mode information is carried viatenantEvidenceMode(control level) andevidenceRefs[].sourceType(per reference). TreatevidenceScopes[]as a future/proposed extension until the model is updated.
tenantEvidenceMode = tenant-supplied MUST NOT be represented as Stella-native telemetry. UI and exports must label it as operator/customer-provided evidence.
For NIS2 control rows, the projection preserves the row’s evidenceMode and each evidenceRefs[].sourceType so consumers can distinguish Stella-native from operator-supplied evidence. A control can be applicable and still have operator-supplied gaps; exports must show that split rather than rendering a single complete Stella-native row.
Note: the only framework adapter wired into this route today is
nis2. DORA (Register of Information, major-incident report, etc.) is exposed through ExportCenter Assurance export profiles (AssuranceExportProfileRegistry), not through the Policy control-register route; see NIS2 Compatibility Mapping.
Area Or Domain Shape
{
"kind": "thematic-area",
"id": "nis2-area-03",
"number": 3,
"name": "Incident handling"
}
number is optional (nullable); frameworks without ordinal grouping omit it. The NIS2 adapter sets kind = thematic-area, id = nis2-area-NN (zero-padded), number = the thematic area, and name from the fixed 13-area name table in AssuranceControlRegisterSchema.ResolveNis2ThematicAreaName. The supported kind constants are thematic-area, standard-clause (AreaKindStandardClause), control-family (AreaKindControlFamily), and custom-domain (AreaKindCustomDomain); the value must be deterministic and documented in the pack.
Evidence Reference Shape
Reflects the AssuranceEvidenceRef record. mediaType, hash, description, submittedByRole, and collectedAt are all optional/nullable.
{
"ref": "policy://control-registers/nis2/snapshots/2026-04",
"sourceType": "stella-native",
"mediaType": "application/json",
"hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"description": "Published NIS2 control-register snapshot",
"submittedByRole": "compliance-lead",
"collectedAt": "2026-04-01T00:00:00Z"
}
sourceType is one of (AssuranceControlRegisterSchema.IsSupportedEvidenceSourceType; any unrecognized value normalizes to external-reference):
stella-nativetenant-suppliedmixedexternal-reference
Note: the NIS2 adapter projects
mediaTypeandhashasnullfor evidence pointers (AssuranceControlRegisterSnapshotBuilder.FromNis2Mapping); those fields are populated only when authored directly via the genericAssuranceControlMappingshape.
NIS2 Compatibility Mapping
assurance-control-register-v1 | nis2-control-register-v1 |
|---|---|
frameworkId | constant nis2 |
packId | constant nis2 |
areaOrDomain.id | nis2-area-NN (zero-padded thematicArea) |
areaOrDomain.number | thematicArea |
externalRefs[] | art21Refs[] (de-duplicated, sorted) |
responsibleRole | responsibleRole |
implementingControls[] | implementingControls[] |
evidenceRefs[] | evidencePointers[] (with mediaType/hash projected as null) |
applicability | metadata.applicability (defaults to applicable) |
applicabilityReason | metadata.applicabilityReason |
tenantEvidenceMode | evidenceMode (the row’s evidence-mode column, normalized) |
reviewCadence.lastReviewed | lastReviewed |
reviewCadence.nextReview | nextReview |
reviewCadence.cadenceLabel | metadata.reviewCadence / metadata.reviewCadenceLabel (defaults to unspecified) |
The existing route GET /api/policy/control-register/nis2 remains the NIS2 compatibility route (returns the nis2-control-register-v1 response shape: thematic-area summaries, controls[], items[], blockers[], cursor).
The generic Policy-owned route is:
GET /api/policy/control-register/frameworks/{frameworkId}
Both read routes are defined in src/Policy/StellaOps.Policy.Engine/Endpoints/Nis2ControlRegisterEndpoints.cs under the /api/policy/control-register group and require the policy:read scope. The generic route uses the same envelope-bound tenant resolution (stellaops:tenant claim; missing tenant -> HTTP 401 TENANT_REQUIRED), optional tenantId query mismatch rejection (HTTP 403 TENANT_MISMATCH), thematicArea (1-13; otherwise HTTP 400 INVALID_THEMATIC_AREA), evidenceMode filter (stella-native | tenant-supplied | mixed; otherwise HTTP 400 INVALID_EVIDENCE_MODE), reviewStatus filter (current | due-soon | overdue; otherwise HTTP 400 INVALID_REVIEW_STATUS), responsibleRole filter (case-insensitive), opaque base64url cursor pagination (default page size 100, max 500), and asOfDate=yyyy-MM-dd replay behavior (otherwise HTTP 400 INVALID_AS_OF_DATE) as the NIS2 compatibility route. An empty/blank frameworkId fails with HTTP 400 INVALID_FRAMEWORK_ID.
A companion authoring route also exists:
POST /api/policy/control-register/nis2
It requires the policy:author scope, accepts a Policy document body ({ "document": "...", "format": "yaml" | "json", "correlationId": "..." }) carrying complianceMappings.controls[], validates it through the Policy binder, and projects each control into policy.control_register_entries via RegisterOrUpdateAsync, emitting control:registered / control:updated lifecycle events.
Current adapters:
frameworkId | packId | Storage source | Notes |
|---|---|---|---|
nis2 | nis2 | policy.control_register_entries (filtered via ListActiveByFrameworkAsync, where a row’s frameworkId/packId default to nis2 unless overridden by metadata.frameworkId/metadata.packId) | First (and currently only) Assurance adapter; preserves the legacy /nis2 route. |
Persistence (migration 014_nis2_control_register.sql) lives in the policy schema and is shared by both the NIS2 and Assurance projections: policy.control_register_entries (one row per (tenant_id, control_id), thematic_area constrained 1-13, status in registered/updated/archived, evidence_mode in stella-native/tenant-supplied/mixed), policy.control_register_events (lifecycle stream: control:registered, control:updated, control:archived, control:snapshot_published), and policy.control_register_snapshots. All three tables enforce row-level tenant isolation.
Unknown framework ids MUST fail closed with a deterministic problem response: HTTP 404 and code UNKNOWN_FRAMEWORK. They must not return an empty successful register because that would hide misconfiguration.
The generic route response (AssuranceControlRegisterResponse) includes schemaVersion, frameworkId, packId, tenantId, generatedAt, asOfDate, source (currently the constant live), sourceOfTruth (currently the constant Policy), detailed controls[], compact items[], cursor, total, sourceRefs, and snapshotHash. For the live NIS2 adapter, sourceRefs is always populated with policyDocRef = policy://control-register/frameworks/nis2, packRef = assurance-pack://nis2, and evidenceBundleRef = null; snapshotHash is currently always null (reserved for stored-snapshot replay). Each controls[] entry also carries ownerRole, reviewStatus, evidenceRefIds[], tenantOverrideRefs[], auditEventIds[], and mappingHash in addition to the AssuranceControlMapping fields.
Determinism Rules
- Controls are sorted by
areaOrDomain.id(ordinal), thencontrolId(AssuranceControlRegisterSnapshotBuilder.SortControls; the HTTP read endpoint applies the same order). A single register response carries oneframeworkId, so framework id is not part of the sort key. externalRefs[]are de-duplicated and sorted ordinally.implementingControls[]are sorted bykind, thenref.evidenceRefs[]are sorted byref, thenhash(empty string when null).metadatakeys are sorted ordinally; blank-key entries are dropped and values are trimmed.- Snapshot hashes use lower-case SHA-256 with
sha256:prefix and are computed over the canonical JSON serialization (CanonJson). - Canonical JSON must not include local filesystem paths unless the path itself is the evidence ref being intentionally recorded.
- Lifecycle event
detailsJsonincludesschemaVersion,frameworkId,packId,thematicArea,evidenceMode, andstatusso downstream export jobs do not need to infer the pack from NIS2-only column names (ControlRegisterRepository).frameworkId/packIdare currently written as the constantsnis2/nis2.
Offline Fixtures
Policy-owned tests should include:
- a NIS2 compatibility fixture — present (
StellaOps.Policy.Engine.Tests/Integration/Nis2ControlRegisterEndpointTests.csexercises both/control-register/nis2and/control-register/frameworks/nis2); - tenant-supplied evidence fixtures that remain labelled as tenant-supplied — present (the integration fixtures assert
evidenceMode/tenantEvidenceModeoftenant-suppliedsurvives the projection); - at least one synthetic non-NIS2 fixture proving the model is not hardcoded to thirteen thematic areas — NOT YET PRESENT. Today the only enabled adapter is
nis2; the generic route fails closed withUNKNOWN_FRAMEWORKfor any other framework id (asserted withcustom-assurance), so thestandard-clause/control-family/custom-domaincode paths are not yet covered by a working fixture. Add such a fixture when a second adapter lands.
