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

FieldTypeRequiredNotes
schemaVersionstringyesMUST be assurance-control-register-v1.
frameworkIdstringyesFramework id such as nis2, iso27001, or soc2. Normalized to lower case.
packIdstringyesPack id from assurance-pack-v1. Normalized to lower case.
tenantIdstringyesTenant that owns the register.
stellaReleasestringyesStella release identifier the snapshot was produced under.
generatedAtRFC3339 timestampyesUTC generation timestamp.
controls[]arrayyesFramework controls sorted deterministically.
sourceRefsobjectnoSource 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).

FieldTypeRequiredNotes
controlIdstringyesStable tenant-visible id.
areaOrDomainobjectyesFramework-specific grouping, for example NIS2 thematic area 1-13.
externalRefs[]string arrayyesFramework article/control/annex refs (for NIS2, derived from art21Refs[]).
policyDocRefstringyesHuman-readable policy or module doc reference.
responsibleRolestringyesRole accountable for review and evidence freshness.
applicabilitystringyesapplicable, not-applicable, partially-applicable, or operator-supplied.
applicabilityReasonstringconditionalRequired unless applicability = applicable (the NIS2 adapter substitutes not specified when missing for non-applicable rows).
implementingControls[]arrayyesReferences to implementing controls (kind, ref, optional description, metadata). For NIS2 the binder accepts kinds gate, scope, metric, runbook.
evidenceRefs[]arrayyesEvidence refs with source labels (see Evidence Reference Shape).
tenantEvidenceModestringyesOne of the four evidence source types (stella-native, tenant-supplied, mixed, external-reference); the NIS2 adapter projects the row’s evidenceMode.
reviewCadenceobjectyeslastReviewed (date), nextReview (date), cadenceLabel (string; defaults to unspecified).
metadataobjectnoString-normalized extension bag.

Note: an evidenceScopes[] per-control field was specified in an earlier draft but is NOT present in the implemented AssuranceControlMapping record and is NOT emitted by the read API. Evidence-mode information is carried via tenantEvidenceMode (control level) and evidenceRefs[].sourceType (per reference). Treat evidenceScopes[] 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):

Note: the NIS2 adapter projects mediaType and hash as null for evidence pointers (AssuranceControlRegisterSnapshotBuilder.FromNis2Mapping); those fields are populated only when authored directly via the generic AssuranceControlMapping shape.

NIS2 Compatibility Mapping

assurance-control-register-v1nis2-control-register-v1
frameworkIdconstant nis2
packIdconstant nis2
areaOrDomain.idnis2-area-NN (zero-padded thematicArea)
areaOrDomain.numberthematicArea
externalRefs[]art21Refs[] (de-duplicated, sorted)
responsibleRoleresponsibleRole
implementingControls[]implementingControls[]
evidenceRefs[]evidencePointers[] (with mediaType/hash projected as null)
applicabilitymetadata.applicability (defaults to applicable)
applicabilityReasonmetadata.applicabilityReason
tenantEvidenceModeevidenceMode (the row’s evidence-mode column, normalized)
reviewCadence.lastReviewedlastReviewed
reviewCadence.nextReviewnextReview
reviewCadence.cadenceLabelmetadata.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:

frameworkIdpackIdStorage sourceNotes
nis2nis2policy.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

Offline Fixtures

Policy-owned tests should include: