NIS2 Effectiveness Report Contract v1
Contract ID: CONTRACT-NIS2-EFFECTIVENESS-REPORT-V1-062 Status: Published (sprint 062 monthly export work) Owner: Export Center Guild Schema version: nis2-effectiveness-report-v1 Bundle schema version: nis2-effectiveness-report-bundle-v1 Input contract: nis2-effectiveness-dashboard-v1 DSSE payload type: application/vnd.stellaops.nis2.effectiveness-report.v1+json
Part of the Stella Ops Contracts index. Audience: Export Center and CLI implementers building the monthly NIS2 effectiveness export, and auditors/reviewers who verify the resulting offline bundle. The input dashboard is produced by the KPI Telemetry Schema read projection; the linked control register is the NIS2 Control Register.
Implementation references (ground truth):
- Adapter:
src/ExportCenter/StellaOps.ExportCenter.Adapters.Nis2/Nis2EffectivenessExporter.cs,Nis2EffectivenessModels.cs,Nis2EffectivenessBundleVerifier.cs. - Schema/payload constants:
Nis2EffectivenessConstants(ReportSchemaVersion,BundleSchemaVersion,SourceReportSchemaVersion,PayloadType,SignatureAlgorithm). - CLI:
src/Cli/StellaOps.Cli/Commands/Nis2EffectivenessCommandGroup.cs(registered asstella export nis2-effectivenessandstella verify nis2-effectiveness). - Assurance profile registration:
AssuranceExportProfileRegistryprofile idnis2.effectiveness-report.
Purpose
The NIS2 effectiveness report export pins the monthly 13-area effectiveness dashboard shape into a deterministic bundle for offline auditor review. The report is evidence-pointer-first: it preserves the dashboard metrics, linked controls, target/control snapshot references, hashes, and blockers that were present at export time.
This contract does not claim production qualified signing. The current CLI path uses a local HMAC DSSE-shaped envelope so the bundle can be reproduced and tamper-checked offline. Production CAdES/KMS signing remains a separate platform integration and must not be inferred from the DSSE signature alg = "HS256".
HMAC operations are not raw System.Security.Cryptography calls: they route through StellaOps.Cryptography.DefaultHmacAlgorithm.Instance.ComputeMac(..., HmacAlgorithmName.HmacSha256) (Sprint 20260430_000 HMAC-CLEANUP-EXPORT-NIS2) so that regional crypto plugins (FIPS/GOST/SM/eIDAS) can substitute the implementation. The DSSE signature alg is always recorded as HS256 regardless of the substituted provider.
Bundle Envelope
{
"schemaVersion": "nis2-effectiveness-report-bundle-v1",
"sourceReportSchemaVersion": "nis2-effectiveness-dashboard-v1",
"sourceReport": {},
"monthlyReport": {},
"manifest": {},
"signature": {}
}
Field notes (from Nis2EffectivenessReportBundle):
| Field | Type | Notes |
|---|---|---|
schemaVersion | string | Always nis2-effectiveness-report-bundle-v1. |
sourceReportSchemaVersion | string | Always nis2-effectiveness-dashboard-v1. |
sourceReport | object | The normalized nis2-effectiveness-dashboard-v1 source report, embedded verbatim. |
monthlyReport | object | The nis2-effectiveness-report-v1 monthly report (see below). |
manifest | object | { "generatedAt", "reportMonth", "entries": [] }. generatedAt and reportMonth mirror the report; entries[] is documented under Manifest Entries. |
signature | object | A single DSSE envelope (Nis2EffectivenessDsseEnvelope); not a detached signature object. See Signing and Verification for its shape. |
The DSSE envelope embedded in signature is:
{
"payloadType": "application/vnd.stellaops.nis2.effectiveness-report.v1+json",
"payload": "<base64 of canonical monthlyReport bytes>",
"signatures": [
{ "keyid": "<key id>", "alg": "HS256", "sig": "<base64 HMAC-SHA256>" }
]
}
Monthly Report
{
"schemaVersion": "nis2-effectiveness-report-v1",
"tenantId": "tenant-a",
"reportMonth": "2026-04",
"generatedAt": "2026-04-30T12:00:00Z",
"period": {
"start": "2026-04-01",
"endExclusive": "2026-05-01"
},
"source": "live",
"rollingWindow": "rolling-30d",
"sourceReportHash": "sha256:...",
"sourceReferences": {
"sourceReportRef": "nis2-effectiveness-dashboard://tenant-a/2026-04#sha256:...",
"targetDocumentHash": "sha256:...",
"targetDocumentRef": "policy://target-documents/nis2/2026-04",
"controlRegisterSnapshotHash": "sha256:...",
"controlRegisterSnapshotRef": "policy://control-registers/nis2/2026-04",
"policySnapshotRef": "policy://snapshots/nis2/2026-04"
},
"areaCount": 13,
"areas": [],
"blockers": [],
"reportHash": "sha256:..."
}
Required Fields
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion | string | yes | Must be nis2-effectiveness-report-v1. |
tenantId | string | yes | Tenant that owns the effectiveness report. |
reportMonth | string | yes | Calendar month in yyyy-MM form. |
generatedAt | timestamp | yes | Copied from the source dashboard report after UTC normalization. |
period.start | date | yes | First day of reportMonth. |
period.endExclusive | date | yes | First day of the following month. |
source | string | yes | Source label from the dashboard report, for example live or contract-fixture. |
rollingWindow | string | yes | Source dashboard rolling window. |
sourceReportHash | string | yes | SHA-256 of the canonical normalized sourceReport. |
sourceReferences | object | yes | Immutable refs and hashes for source dashboard, target overrides, control register snapshot, and Policy snapshot when available. |
areaCount | number | yes | Must equal 13. |
areas[] | array | yes | Exactly 13 areas, numbered 1 through 13 exactly once. |
blockers[] | array | yes | Explicit gaps from the source report or export adapter. Empty only when all required upstream references are pinned. |
reportHash | string | yes | sha256:-prefixed SHA-256 over the canonical JSON of the report hash payload (Nis2EffectivenessReportHashPayload), which excludes reportHash itself. |
Area Shape
Each areas[] item (Nis2EffectivenessReportArea) preserves the source dashboard values:
areaNumber,areaSlug,areaLabelevidenceMode,responsibleRole,statelatestObservedAt,nextReview(nullable timestamps, UTC-normalized)override(Nis2EffectivenessSourceOverride:active, optionalowner,reason,effectiveFrom,nextReview,previousValueHash,newValueHash,approvedBy)metrics[](Nis2EffectivenessSourceMetric:kpiKey,metricName,label,description,unit,thresholdDirection,target,latest,trend)linkedControls[](Nis2EffectivenessSourceLinkedControl:controlId,label,route,responsibleRole,evidenceMode,evidenceRefs[],nextReview)
Note: the source dashboard area carries a per-area rollingWindow, but the report area drops it (ToReportArea does not copy it). The report-level rollingWindow is the authoritative window for the monthly report.
The adapter sorts areas by areaNumber, metrics by metricName then kpiKey, linked controls by controlId then label, and evidence refs lexicographically. Validation requires each area to carry at least one metric, and the source report to contain exactly 13 areas numbered 1 through 13 exactly once (ValidateSourceReport).
Blockers
The exporter records blockers instead of fabricating upstream evidence. Each blockers[] item (Nis2EffectivenessReportBlocker) has four fields: code, summary, detail, and source (provenance of the blocker). Blockers are sorted by code, then summary, then detail, then source.
| Code | source | Meaning |
|---|---|---|
source:<code> | nis2-effectiveness-dashboard-v1 | Blocker propagated from the source dashboard report (the original code is prefixed with source:). |
source-contract-fixture | exportcenter | Emitted when the source report’s source field equals contract-fixture; the bundle is deterministic format evidence only, not live tenant telemetry. |
target-document-hash-missing | exportcenter | No target document hash was present in the source report or CLI option. |
control-register-snapshot-hash-missing | exportcenter | No Policy control-register snapshot hash was present in the source report or CLI option. |
policy-snapshot-ref-missing | exportcenter | No Policy snapshot ref was supplied for replay pinning. |
Manifest Entries
manifest carries top-level generatedAt and reportMonth plus an entries[] array. Each entry (Nis2EffectivenessBundleEntry) is { "path", "mediaType", "sha256" }, where sha256 is the sha256:-prefixed SHA-256 of the canonical bytes of the referenced object. The manifest must contain exactly these three logical paths and media types:
| Path | Media type | sha256 is over |
|---|---|---|
source/nis2-effectiveness-dashboard.json | application/vnd.stellaops.nis2.effectiveness-dashboard.v1+json | canonical sourceReport bytes |
reports/nis2-effectiveness-report.json | application/vnd.stellaops.nis2.effectiveness-report.v1+json | canonical monthlyReport bytes |
signatures/nis2-effectiveness-report.dsse.json | application/vnd.dsse+json | canonical signature (DSSE envelope) bytes |
The reports/... entry media type is the same value as the DSSE payloadType (Nis2EffectivenessConstants.PayloadType).
Determinism Rules
- The source dashboard report must contain exactly 13 areas numbered 1 through 13.
- Canonical JSON is used for the source report, monthly report, DSSE envelope, and outer bundle.
sourceReportHashis computed after source normalization and before bundle assembly.reportHashis computed over{ schemaVersion, tenantId, reportMonth, generatedAt, period, source, rollingWindow, sourceReportHash, sourceReferences, areaCount, areas, blockers }.- The bundle hash is the
sha256:-prefixed SHA-256 of the canonicalnis2-effectiveness-report-bundle-v1bytes (CanonJson.Sha256Prefixed). - Re-running export with identical source report, month, source refs, and signing key must produce byte-identical bundle output.
Signing and Verification
- CLI/offline signing uses DSSE PAE (
DSSEv1 <len> <payloadType> <len> <payload>) with payload typeapplication/vnd.stellaops.nis2.effectiveness-report.v1+jsonand local HMAC-SHA256 signatures recorded withalg = "HS256". The signed payload is the canonical bytes ofmonthlyReport, base64-encoded into the DSSEpayloadfield. - HMAC computation and verification both route through
StellaOps.Cryptography.DefaultHmacAlgorithm.Instance.ComputeMac(..., HmacAlgorithmName.HmacSha256); verification usesCryptographicOperations.FixedTimeEqualsfor the constant-time comparison. - Offline verification (
Nis2EffectivenessBundleVerifier.Verify) validates bundle schema versions, source-report schema, the 13-area count, manifest entry hashes, source report hash, report hash, DSSE payload bytes, DSSE payload type, the key id (the signature whosekeyidmatches the supplied signing key id), and the HMAC signature. - A tampered source report, monthly report, manifest, or signature must fail verification with a non-zero CLI exit code (
CliExitCodes.VerificationFailed). - Production ExportCenter signing must use the platform Signer/KMS/CAdES path when that integration is available. Until then, local HMAC output is an offline reproducibility/tamper-evidence mechanism only.
Verification error codes
Verify returns IsValid plus a list of machine-readable error codes. The codes include:
| Code | Cause |
|---|---|
bundle_json_invalid:<exception> / bundle_json_empty | Bundle bytes are not valid JSON / deserialized to null. |
bundle_schema_version_mismatch | schemaVersion is not nis2-effectiveness-report-bundle-v1. |
source_report_schema_version_mismatch / source_report_schema_mismatch | Bundle-level or embedded source-report schema is not nis2-effectiveness-dashboard-v1. |
source_report_missing / monthly_report_missing / manifest_missing / signature_missing | A required top-level section is absent. |
report_schema_version_mismatch | monthlyReport.schemaVersion is not nis2-effectiveness-report-v1. |
report_area_count_mismatch | areaCount != 13 or areas.length != 13. |
source_report_hash_mismatch | Recomputed source report hash differs from monthlyReport.sourceReportHash. |
report_hash_mismatch | Recomputed report hash differs from monthlyReport.reportHash. |
manifest_entries_missing / manifest_entry_missing:<path> / manifest_entry_hash_mismatch:<path> | Manifest entry absent or its sha256 does not match recomputed bytes. |
dsse_payload_type_mismatch | DSSE payloadType is not the expected payload type. |
dsse_payload_invalid_base64 / dsse_payload_mismatch | DSSE payload is not base64 / does not equal the canonical monthly report bytes. |
dsse_signature_missing / dsse_signature_invalid_base64 / dsse_signature_mismatch | No signature matches the supplied key id+alg / sig is not base64 / HMAC does not match. |
CLI Surface
The registered CLI preserves a deterministic local/offline path and can also delegate production to the shipped ExportCenter Assurance run endpoint. The local path resolves signing material from operator-controlled CLI inputs; --via-export-center posts the same source dashboard to ExportCenter, where signing material is resolved server-side, and downloads the produced bundle.
stella export nis2-effectiveness(Nis2EffectivenessCommandGroup.BuildNis2EffectivenessExportCommand). Options:--month(required,yyyy-MM),--report(required localnis2-effectiveness-dashboard-v1JSON for both local and ExportCenter production),--via-export-center,--output/-o,--overwrite,--signing-key-file,--signing-key-id,--source-report-ref,--target-document-ref,--target-document-hash,--control-register-snapshot-ref,--control-register-snapshot-hash,--policy-snapshot-ref,--json. Without--via-export-center, the command produces and self-verifies the bundle locally and returnsVerificationFailedif verification fails. With--via-export-center, it starts the Assurance run, downloads the server-produced bundle, and still supports offline verification of the downloaded artifact. When--outputis omitted the bundle is written tonis2-effectiveness-<tenantId>-<yyyyMM>.bundle.json.stella verify nis2-effectiveness(BuildNis2EffectivenessVerifyCommand). Options:--bundle(required),--signing-key-file,--signing-key-id,--json.- Signing key resolution (
ResolveSigningKeyAsync):--signing-key-fileif supplied, otherwise theSTELLAOPS_NIS2_EFFECTIVENESS_HMAC_KEYenvironment variable; the key bytes are UTF-8 trimmed before HMAC use, and the key id defaults tohmac-sha256:<first16hex>when--signing-key-idis omitted.
The Assurance profile registry advertises the registered compatibility command exactly as stella export nis2-effectiveness.
Assurance Profile Discovery And Run Execution (HTTP)
The nis2.effectiveness-report profile is discoverable and executable through the framework-aware Assurance export API (AssuranceExportEndpoints):
GET /v1/exports/assurance/profiles(optionally?frameworkId=nis2; scopeexport.viewer)GET /v1/exports/assurance/profiles/{profileId}(nis2.effectiveness-report; scopeexport.viewer)GET /v1/exports/assurance/profiles/{profileId}/readiness(scopeexport.viewer)POST /v1/exports/assurance/profiles/nis2.effectiveness-report/runs(scopeexport.operator) starts deterministic signed production from the suppliednis2Effectivenesssource inputs.GET /v1/exports/assurance/profiles/nis2.effectiveness-report/runs/{runId}(scopeexport.viewer) reads the run.GET /v1/exports/assurance/runs/{runId}/bundle(scopeexport.viewer) downloads the produced bundle.
When operator-owned signing and storage configuration is ready, profile readiness reports the non-blocking source-dashboard-required reason because the monthly NIS2 effectiveness dashboard remains a produce-time input. Missing signing material fails closed instead of falling back to request-supplied secrets.
