EU Regulatory Artifact Ledger v1
Version: eu-regulatory-artifact-ledger.v1 Owner: EvidenceLocker Status: implemented Sprint: SPRINT_20260430_200
Part of the Stella Ops Contracts index. Audience: implementers and reviewers of the EvidenceLocker regulatory replay index and any downstream module (Graph, CLIs) that reads it through the HTTP API.
Companion contract: eu-regulatory-audit-events-v1.md— the audit events referenced by the ledger.
Source of truth (code):
- Domain model and id derivation:
src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Core/Regulatory/RegulatoryArtifactLedger.cs - Audit-event names, regimes, artifact types, actor kinds:
.../Regulatory/RegulatoryAuditEvent.cs - Retention rules:
.../Regulatory/RegulatoryRetentionRules.cs - HTTP read API:
src/EvidenceLocker/StellaOps.EvidenceLocker/Api/RegulatoryArtifactLedgerEndpoints.cs - Persistence repository:
src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Repositories/RegulatoryEvidenceRepository.cs - Migrations:
src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/Migrations/001_v1_evidencelocker_baseline.sqland019_regulatory_retention_horizon.sql(the pre-1.0007migration is archived and is not embedded at runtime).
Purpose
The regulatory artifact ledger is the EvidenceLocker-owned replay index for CRA reports, NIS2 incident packets, DORA reports, RoI snapshots, TLPT evidence packs, and standards-mapping exports. Downstream modules write or call this contract rather than inventing local persistence for regulatory artifact identity, schema pins, signatures, approvals, or retention.
Artifact Type Enum
| Value | Use |
|---|---|
cra-advisory-report | CRA vulnerability/advisory report evidence. |
cra-technical-file | CRA Annex VII technical-file bundle. |
cra-conformity-dossier | CRA conformity/self-assessment dossier. |
nis2-incident-packet | NIS2/customer CSIRT reporting packet. |
nis2-control-register-control | Hash-only projection of a Policy NIS2 control lifecycle event. |
nis2-control-register-snapshot | Policy NIS2 control-register snapshot pinned by content hash and source refs. |
nis2-statement-of-applicability | NIS2 SoA export hash pinned to the source control-register snapshot. |
dora-incident-report | DORA major-incident report artifact. |
dora-roi-snapshot | Annual DORA Register of Information snapshot. |
tlpt-evidence-pack | DORA TLPT signed evidence pack capsule. |
standards-mapping-export | ISO/IEC/ETSI/JRC mapping export. |
public-product-metadata | CRA support/security public metadata. |
Regime Enum
regime is one of (RegulatoryRegimes consts; DB CHECK (regime IN (...))):
| Value | Use |
|---|---|
cra | EU Cyber Resilience Act artifacts. |
nis2 | NIS2 directive artifacts. |
dora | DORA (Digital Operational Resilience Act) artifacts. |
standards | ISO/IEC/ETSI/JRC standards-mapping artifacts. |
Values are normalized to lower case before persistence (RegulatoryValueNormalizer.NormalizeRequired).
Ledger Fields
Each record (RegulatoryArtifactLedgerRecord) stores:
ledgerRecordId: deterministic UUID from tenant, regime, artifact type, and content hash. Derived byRegulatoryArtifactLedgerIds.Create(...): SHA-256 over the newline-joined normalized fields, truncated to 16 bytes with the UUID version nibble forced to5(0x50) and the IETF variant bits set.tenantId: UUID tenant id.regime:cra,nis2,dora, orstandards.artifactType: enum value above.contentHash:sha256:<64 lowercase hex>for the produced artifact. Stored in thecontent_hashcolumn; surfaced asartifactHashon the HTTP read API.sourceEvidenceRefs[]: deduplicated, ordinal-sorted opaque refs to bundles, capsules, or input evidence. These are refs and hashes, not copied runtime state. Stored as a JSONB array (source_evidence_refs); surfaced asevidenceRefson the HTTP read API.schemaPinId: Stella contract/schema pin used for the artifact (required).taxonomyPinId: optional external/local taxonomy package marker.signatureRef: optional DSSE or signature bundle reference.signerKeyId: key id used when signed.approvalAuditEventId,generationAuditEventId,signingAuditEventId: optional links intoeu-regulatory-audit-events.v1. The HTTP read API collapses the non-null ids into a singleauditEventIds[]array.retentionRecordId: optional link into the retention metadata record.createdAt: UTC creation timestamp supplied by the caller (column defaultNOW() AT TIME ZONE 'UTC'when omitted at the SQL layer).
Retention Metadata
Retention records (RegulatoryRetentionRecord) live in evidence_locker.regulatory_retention_records and carry:
retentionRecordId: deterministic UUID from tenant, artifact type, and artifact hash (RegulatoryRetentionRecordIds.Create(...), same SHA-256/v5 derivation as the ledger id).tenantId: UUID tenant id.artifactType: artifact type the retention record governs.artifactHash:sha256:<64 lowercase hex>of the governed artifact.retentionPolicyId: e.g.dora-roi-annual-v1for the default DORA RoI policy.retentionYears: integer, must be> 0(DBCHECK).retainUntil: UTC instant that must be at or aftercreatedAt.ToUniversalTime().AddYears(retentionYears). This is a calendar-year relationship with leap-day semantics, not a fixed number of days.legalHoldUntil: optional; must be>= retainUntil(DBck_regulatory_retention_legal_hold_extends).overrideAuditEventIdshorteningApprovalRefcreatedAt,updatedAt: UTC timestamps.
Retention rules (RegulatoryRetentionRules): default DORA RoI retention is 7 years (DefaultDoraRoiRetentionYears). Tenant RoI overrides are bounded to 5-10 years (Minimum/MaximumDoraRoiRetentionYears). TLPT evidence-pack retention has a 10-year minimum (MinimumTlptRetentionYears); stricter tenant policies can extend it. Shortening an existing retain-until date fails closed (InvalidOperationException) unless shorteningApprovalRef is present. The approval never permits a record below its own declared created-at horizon, and legal hold is extension-only. Core validation rejects default/overflowing creation timestamps. Database migration 019_regulatory_retention_horizon.sql enforces the relationship against the persisted original created_at on every new or updated row. Its initial NOT VALID state leaves historical-row audit as an explicit operator action before VALIDATE CONSTRAINT.
Determinism
RegulatoryArtifactLedgerIds.Create(...) computes the ledger id from normalized tenant id, regime, artifact type, and content hash. sourceEvidenceRefs[] are deduplicated and ordinal sorted before persistence. Replay lookup is by (tenantId, regime, artifactType, contentHash), enforced as a unique constraint (uq_regulatory_artifact_lookup); upserts collapse to that key.
HTTP Read API
EvidenceLocker exposes a single source-owned read endpoint. Downstream services (Graph, CLIs, etc.) must call this API or a typed client over it rather than reading the tables directly.
GET /api/v1/regulatory/artifact-ledger- Auth: requires the
evidence:readscope (StellaOpsResourceServerPolicies.EvidenceRead→StellaOpsScopes.EvidenceRead) and a resolved tenant (RequireTenant()). No write endpoint is exposed; writes go throughIRegulatoryEvidenceRepositoryin-process. - Query parameters (all optional):
regime— filter to one regime.artifactType— filter to one artifact type.reportingYear— filter by reporting year (year ofcreatedAt).limit— page size; clamped to1..500(default 100).cursor— opaque base64offset:<n>pagination cursor.requireMatch— whentrue, fail-closed lookup:regime,artifactType, andreportingYearare all mandatory and a non-empty match is required.
- Responses:
200 OK→RegulatoryArtifactLedgerResponsewith fieldsschemaVersion("eu-regulatory-artifact-ledger.v1"),items[],cursor(next-page cursor ornull), andsourceOfTruth("EvidenceLocker").400 Bad Request→{ "code", "message" }withcodeofinvalid_request(missing requiredrequireMatchfields, or repositoryArgumentException) orinvalid_cursor.404 Not Found→{ "code": "artifact_not_found", "message": ... }only whenrequireMatch=trueand no row matches.
Each items[] entry (RegulatoryArtifactLedgerItem) is JSON with: artifactId (the ledgerRecordId), regime, artifactType, reportingYear (year of createdAt), artifactHash (the contentHash), evidenceRefs[], auditEventIds[] (non-null approval / generation / signing ids), schemaPinId, taxonomyPinId, signatureRef, signerKeyId, retentionRecordId, and createdAt.
Results are ordered by reporting year descending, then artifactType ascending, contentHash ascending, and ledgerRecordId ascending. reportingYear filtering matches EXTRACT(YEAR FROM created_at AT TIME ZONE 'UTC').
There is currently no CLI command bound to this contract.
Persistence And Migration
Migration 007_regulatory_audit_retention_ledger.sql adds:
evidence_locker.regulatory_audit_eventsevidence_locker.regulatory_retention_recordsevidence_locker.regulatory_artifact_ledger- optional retention/ledger reference columns (
regulatory_retention_record_id,regulatory_artifact_ledger_id) onevidence_locker.evidence_bundlesandevidence_locker.decision_capsules, each with a partial index on(tenant_id, regulatory_retention_record_id).
Tenancy and integrity:
- All three EU tables use a composite primary key
(<id>, tenant_id). - All three have
ENABLE/FORCE ROW LEVEL SECURITYwith an isolation policy keyed ontenant_id = evidence_locker_app.require_current_tenant(). regulatory_artifact_ledgerenforcesuq_regulatory_artifact_lookup(tenant_id, regime, artifact_type, content_hash), aregimeCHECKagainst the four regimes, acontent_hashCHECKmatching^sha256:[0-9a-f]{64}$, and aCHECKthatsource_evidence_refsis a JSON array.regulatory_audit_eventsadditionallyCHECKsactor_kind IN ('stella-manufacturer', 'customer-operator')andevent_hash/artifact_hashagainst the sha256 pattern.
The migration is embedded in the EvidenceLocker infrastructure assembly and is applied by the startup migration hosted service (EvidenceLockerMigrationHostedService) when EvidenceLocker:Database:ApplyMigrationsAtStartup=true (the default; EvidenceLockerOptions.Database.ApplyMigrationsAtStartup).
Offline Replay
Replay requires only the ledger rows, audit event rows, source evidence refs, schema/taxonomy assets already pinned in-repo or runtime assets, and signature material. The ledger must not store mutable report payloads, live regulator responses, credentials, or expanded human PII.
NIS2 Control Register Projection
Policy remains the source of truth for nis2-control-register-v1 entries, lifecycle events, and snapshot JSON in policy.control_register_*. When Policy is configured with the EvidenceLocker regulatory repository, control lifecycle events are projected as evidence.eu.control.lifecycle_recorded audit events using the Policy event id as the audit event id. Published snapshots create hash-only artifact ledger rows for nis2-control-register-snapshot and nis2-statement-of-applicability; their sourceEvidenceRefs[] point back to the Policy snapshot/event refs and the EvidenceLocker bundle ref instead of copying control state into the ledger.
