NIS2 Incident Report Envelope v1
Contract ID: CONTRACT-NIS2-INCIDENT-REPORT-ENVELOPE-V1-028 Status: Draft Owner: Notify platform Contract version: nis2-incident-report-envelope.v1
Part of the Stella Ops Contracts index. Audience: Notify implementers building the Member-State CSIRT connector, and operators who configure NIS2 reporting channels. This envelope is a deterministic operator submission aid; it does not make Stella Ops the regulated entity (see Purpose).
Runtime library: src/Notify/__Libraries/StellaOps.Notify.Connectors.NCS/
Adjacent tests: src/Notify/__Tests/StellaOps.Notify.Connectors.NCS.Tests/
Notify plugin id: stellaops.notify.connector.ncs (display name “StellaOps NIS2 Member-State CSIRT Connector”; version 0.1.0-alpha; requiresRestart: true). The connector binds to NotifyChannels whose purpose is nis2-csirt (NotifyChannelPurpose.Nis2Csirt) and whose type is custom (the plugin metadata default delivery mode is signed-json-webhook).
Purpose
This contract defines Stella’s local, deterministic NIS2 Article 23 incident report envelope for customer/operator handoff to Member-State CSIRT/NCA channels. It is an operator submission aid. It does not make Stella the regulated entity and it does not hardcode national portal endpoints.
Versioning
The version strings below are the canonical constants on Nis2CsirtConstants (StellaOps.Notify.Connectors.NCS). The connector rejects any input whose contractVersion / localEnvelopeSchemaVersion does not match these exact values (Nis2CsirtValidator.RequireExact).
- Contract version (
Nis2CsirtConstants.ContractVersion):nis2-incident-report-envelope.v1 - Local envelope schema version (
Nis2CsirtConstants.LocalEnvelopeSchemaVersion):nis2-csirt-local-envelope-v1 - Local validation rules (
Nis2CsirtConstants.LocalValidationRuleSetVersion):nis2-csirt-local-validation-v1 - Envelope media type (
Nis2CsirtConstants.EnvelopeMediaType):application/vnd.stellaops.nis2-csirt-report.v1+json - Schema pinning policy: Stella minor releases pin this local envelope and the connector adapter behavior. Official Member-State portal schemas can move from
blockedonly after the exact contract package is vendored, license-reviewed, and covered by local tests.
Report Types
reportType is emitted as a stable string id (Nis2CsirtValidator.ReportTypeId) from the Nis2CsirtReportType enum:
early_warning_24h(EarlyWarning24h)incident_notification_72h(IncidentNotification72h)final_report_1m(FinalReport1m)
Envelope Fields
The connector writes compact JSON (Utf8JsonWriter, not indented) with stable property order and arrays sorted ordinally. Field names and order are fixed by Nis2CsirtEnvelopeBuilder.BuildEnvelope; the envelope is sha256-hashed (sha256:<64 lowercase hex>) and the hash is re-verified by Nis2CsirtValidator.ValidatePackage before signing. Top-level properties, in emit order:
contractVersion,localEnvelopeSchemaVersion,reportType,reportIdtenantId,channelId,memberStateCode,authorityDisplayName,profileIdacceptedFormat: one ofsigned-json-webhook,csaf,webform,pgp-email(Nis2CsirtValidator.AcceptedFormatId)generatedAtas UTC ISO-8601 with milliseconds (yyyy-MM-ddTHH:mm:ss.fffZ)operatorApprovalRefreporter:legalEntityName,countryCode(ISO 3166-1 alpha-2),contactRef,complianceProfileRefincident:incidentId,title,summary,severity,status,startedAt, then optional (nullable)detectedAt,closedAt,suspectedCause,impactSummary,crossBorderImpactSummary,rootCauseSummary,remediationSummaryclassification:significantImpact(bool),criteria(array),rationaletimeline:timelineRef, thenopenedAt,earlyWarningDueAt(24-hour),notificationDueAt(72-hour),finalReportDueAt(one-month), andpreviousTransmissionHashes(array ofsha256:...)affectedServices(each:serviceId,name,category,impactSummary,assetRefs),indicators(each:indicatorId,type,value, optionalfirstSeenAt),actions(each:actionId,description,status, optionalperformedAt)attestationRefs,evidenceRefs(each must be non-empty)localValidation: object withstatusandruleSetVersion(nis2-csirt-local-validation-v1)memberStatePortalContractValidation: object withstatus,reason,evidence(array)
Severity, status, indicator type, action status, and service category values are lower-cased during normalization. Optional timestamp/string fields are written as JSON null when absent rather than omitted.
Validation is report-type aware: 72h and final reports additionally require at least one classification.criteria entry, a classification.rationale, at least one affectedServices entry, and at least one actions entry. Final reports must also include incident.closedAt, incident.rootCauseSummary, incident.remediationSummary, and at least one timeline.previousTransmissionHashes entry (each a valid sha256:<64 hex>).
Delivery Modes
The connector resolves one of three delivery modes (Nis2CsirtChannelOptionsParser.ResolveDeliveryMode) from the channel deliveryMode property, falling back to the channel type (Email -> pgp-email, Webhook -> signed-json-webhook, otherwise member-state-portal-stub):
member-state-portal-stub(MemberStatePortalStubDeliveryMode): requires the member state to be DE/FR/NL and an absolute HTTPSingestionUrl; honorsrequiredHeaders. Submitted viaINis2CsirtSubmissionClientwhen one is wired; otherwise the request is prepared but not sent.signed-json-webhook(SignedJsonWebhookDeliveryMode): requires an absolute HTTPSwebhookUrl.pgp-email(PgpEmailDeliveryMode): requires a valid operator email, a full OpenPGPpgpRecipientFingerprint(40 or 64 hex), and apgpPublicKeyRef.
Audit event types are emitted per outcome (Nis2CsirtDeliveryAuditEvent), e.g. nis2.csirt.portal_stub.submitted / .prepared, nis2.csirt.signed_webhook.submitted / .prepared, nis2.csirt.pgp_email.encrypted / .blocked.
Signing And Handoff
The envelope is signed through INis2CsirtReportSigner. The production adapter available in this slice is Nis2CsirtSignerPipelineSigner, which maps the envelope into the SignerPipeline DSSE flow (ISignerPipeline, KMS signing mode) with predicate type:
https://stellaops.org/predicates/nis2-csirt-report/v1
The resulting DSSE envelope uses payload type application/vnd.in-toto+json and the signature profile id signer-pipeline-in-toto. The signer returns the DSSE envelope JSON, its sha256, the signed subject sha256, the signature profile, and the optional Signer audit id and key id.
Signed-webhook (and portal-stub) handoff wraps the envelope and DSSE envelope in a body with Content-Type application/vnd.stellaops.nis2-csirt-signed-handoff.v1+json (SignedHandoffMediaType) and sends Stella headers. Member-State profiles declare five required headers; the connector adds two more at submission time (X-StellaOps-Body-SHA256, X-StellaOps-Signature-Profile) plus Content-Type. Operator-configured header.* properties are merged in first. Full header set:
X-StellaOps-NIS2-Contract-VersionX-StellaOps-NIS2-Member-StateX-StellaOps-Report-IdX-StellaOps-Envelope-SHA256X-StellaOps-DSSE-SHA256X-StellaOps-Body-SHA256(added at submission)X-StellaOps-Signature-Profile(added at submission)
Member-State Profiles
Initial local stub profiles exist for (Nis2CsirtMemberStateProfiles):
DE: BSI/CERT-Bund profile (profileIdnis2-csirt-de-bsi-cert-bund-local-stub-v1)FR: ANSSI/CERT-FR profile (profileIdnis2-csirt-fr-anssi-cert-fr-local-stub-v1)NL: NCSC-NL profile (profileIdnis2-csirt-nl-ncsc-nl-local-stub-v1)
Any other member state resolves to a generic Operator-configured NIS2 CSIRT/NCA profile (profileId nis2-csirt-generic-operator-configured-v1), but member-state-portal-stub delivery is restricted to DE/FR/NL.
These profiles intentionally do not contain live portal URLs or official API contracts. Operators configure ingestionUrl, acceptedFormat, and any portal-required headers per tenant/channel.
memberStatePortalContractValidation.status is blocked (Nis2CsirtConstants.PortalContractValidationStatus) until exact local portal contracts are available, and the connector’s ValidatePackage step enforces that the envelope’s status stays blocked.
Channel Configuration
Per-tenant/channel settings are read from NotifyChannel.Config.Properties (Nis2CsirtChannelOptionsParser). Each key is accepted either bare (e.g. memberStateCode) or prefixed with notify.channel.nis2. (e.g. notify.channel.nis2.memberStateCode):
deliveryMode:member-state-portal-stub|signed-json-webhook|pgp-email(defaults from channel type if unset)memberStateCode(ormemberState): ISO 3166-1 alpha-2acceptedFormat: parsed leniently —signed-json/signed-json-webhook/webhook/empty ->signed-json-webhook;csaf;webform/form->webform;pgp/pgp-email/email->pgp-emailingestionUrlorwebhookUrl(falls back to channelEndpoint/Target): must be an absolute HTTPS URI for portal-stub/webhook deliveryemailAddress(falls back to channelTarget): operator recipient emailrequiredHeaders: comma/semicolon/pipe-separated header names that must be present (portal-stub mode)header.<Name>(ornotify.channel.nis2.header.<Name>): operator-supplied outbound header values, merged into the handoff requestpgpRecipientFingerprint(orpgpFingerprint): full OpenPGP fingerprint (40 or 64 hex, normalized upper-case)pgpPublicKeyRef: reference resolved byINis2PgpPublicKeyResolver
PGP Email Status
PGP-email fallback is available through the approved crypto-boundary contract (OpenPgpEncryptionProviderContract.ContractVersion; see OpenPGP Encryption Provider v1):
StellaOps.Cryptography.OpenPgpEncryptionProvider/v1
The boundary is the INis2PgpEmailEncryptor interface; the production adapter is Nis2OpenPgpEmailEncryptor, which wraps IOpenPgpEncryptionProvider (StellaOps.Cryptography.Plugin.OpenPgp) and an INis2PgpPublicKeyResolver. Notify resolves operator-supplied public key material by pgpPublicKeyRef, validates the full recipient fingerprint, encrypts the signed handoff through the provider, and records the ciphertext SHA-256, provider id, recipient fingerprint, and selected encryption-key fingerprint in delivery metadata.
PGP-email preparation carries a status of blocked, prepared, or encrypted (Nis2CsirtConstants.PgpEncryptionStatus*). The connector fails closed (status blocked, delivery status Failed) if no INis2PgpEmailEncryptor is provided; the adapter additionally throws if the key reference cannot be resolved (resolved ref must equal the requested pgpPublicKeyRef), if the recipient fingerprint does not match the key material, or if the provider rejects the key for expiry, revocation, or missing encryption-key capability. Notify code never calls OpenPGP libraries or shells out to gpg directly.
Delivery Ledger Metadata
A successful or prepared handoff produces a NotifyDelivery ledger entry with metadata keys prefixed nis2. (Nis2CsirtOutboundChannel.BuildDeliveryLedgerEntry), including nis2.acceptedFormat, nis2.audit.eventType, nis2.contractVersion, nis2.deliveryMode, nis2.dsse.sha256, nis2.envelope.sha256, nis2.localSchemaVersion, nis2.memberState, nis2.portalContractValidation, nis2.reportId, nis2.reportType, and nis2.signatureProfile. Submission, signer, and PGP outcomes add conditional keys (e.g. nis2.submission.receiptId, nis2.signer.auditId, nis2.signer.keyIdHash, nis2.pgp.status, nis2.pgp.ciphertextSha256, nis2.pgp.recipientFingerprint). Endpoint and key ids are stored as truncated SHA-256 hashes, never in plaintext. Delivery status is Failed for a blocked PGP attempt, Queued when a submission request is prepared but no submission client returned a receipt, and Sent once a receipt is recorded.
Offline Expectations
All tests must run without internet access, live national portals, SMTP, or webhook endpoints. Current offline proof covers:
- deterministic envelope byte stability
- DE/FR/NL stub submission contracts
- signed-webhook hash and DSSE verification with a local test signer
- production WebService signed-webhook submission path with a local HTTPS stand-in receiver, offline DSSE/body verification, and Notify delivery repository readback; this is not external CSIRT/NCA delivery evidence
- fail-closed PGP-email blocker when no provider is wired
- PGP-email encryption, ciphertext hash evidence, local decrypt, and integrity verification with generated local test keys
- final-report validation for previous transmission hashes
