contract_id: CONTRACT-ENISA-INCIDENT-REPORTING-V1-026 schema_version: enisa-incident-reporting.v1 status: Draft owner: Notify platform schema_reviewer: maintainer-of-record review_cadence: per-stella-minor-release-and-on-official-srp-schema-change legal_sources:
- Cyber Resilience Act Article 14 local_envelope_schema_version: enisa-incident-reporting-local-envelope-v1 enisa_srp_schema_version: BLOCKED-no-local-enisa-srp-schema runtime_library: src/Notify/__Libraries/StellaOps.Notify.Connectors.Enisa/ adjacent_tests: src/Notify/__Tests/StellaOps.Notify.Connectors.Enisa.Tests/ last_updated: 2026-06-17
ENISA Incident Reporting Contract v1
Part of the Stella Ops Contracts index. Audience: implementers and reviewers working on the Notify ENISA CRA connector, its signing flow, and the operator filesystem handoff.
Purpose
This contract defines Stella Ops’ deterministic local handoff envelope for CRA Article 14 incident and actively exploited vulnerability reports intended for ENISA’s Single Reporting Platform (SRP).
The v1 implementation is a produce, sign, and handoff surface:
- build a version-pinned local JSON envelope
- validate the envelope with deterministic in-repo rules
- sign the envelope through the SignerPipeline adapter
- create a Notify delivery-ledger entry
- drop the signed handoff bundle on the operator filesystem by default
- when auto-submit is enabled (opt-in, per-channel/tenant, approved), POST the signed envelope to the configured SRP endpoint over HTTP, falling back to the operator filesystem drop on any pre-transmit gate failure or HTTP error
The connector does not claim official ENISA SRP schema validation in this slice. No live ENISA endpoint or external schema fetch is required at runtime or test time.
The missing official ENISA SRP schema is not a standing test blocker. Tests should assert the local envelope contract, auto-submit transport behavior, the honest default blocked official-schema status, and offline operator-supplied schema-source mechanics only. Do not fabricate, fetch, or vendor an unofficial schema to satisfy conformance. When an official package is published or an operator supplies an approved local asset, open a new asset-intake sprint for license review, SHA-256 pinning, exact local-validator selection, and conformance tests.
The connector ships as a Notify channel plugin (stellaops.notify.connector.enisa, channel type Custom, channel purpose enisa-cra), not as a standalone HTTP service. It exposes no HTTP routes and requires no dedicated OAuth scope of its own; signing flows through the SignerPipeline using the caller’s signer:sign scope. Code lives in src/Notify/__Libraries/StellaOps.Notify.Connectors.Enisa/. The public entry points are EnisaIncidentReportEnvelopeBuilder.EncodePackage (encode + local validation) and EnisaIncidentReportExportService.ExportSignedAsync (encode, sign, build delivery-ledger entry + audit event), with optional EnisaOperatorFilesystemDropWriter.WriteAsync for the filesystem handoff and EnisaSrpHttpTransport.DeliverSignedAsync for the auto-submit HTTP transport.
Schema Pinning
- Contract version:
enisa-incident-reporting.v1 - Local envelope schema version:
enisa-incident-reporting-local-envelope-v1 - Official ENISA SRP schema version:
BLOCKED-no-local-enisa-srp-schema - Local validation ruleset:
enisa-incident-reporting-local-validation-v1 - Local envelope media type:
application/vnd.stellaops.enisa-incident-reporting.v1+json - DSSE predicate type:
https://stellaops.org/predicates/enisa-incident-report/v1 - SignerPipeline payload type:
application/vnd.in-toto+json
(All pin values are defined as constants in EnisaIncidentReportConstants.)
Official SRP schema validation remains blocked until the exact ENISA SRP schema package is vendored in-repo or supplied in offline runtime assets, version-pinned, and license-reviewed. The connector must not fetch schemas from the internet. The available operator-supplied path is JSON Schema only: FileEnisaSrpSchemaSource reads local bytes, verifies the configured SHA-256 pin, disables remote $ref fetching, and evaluates the produced envelope with JsonSchema.Net. That path is a readiness mechanism for a future reviewed asset; it is not an ENISA conformance claim while no official asset is present. Absent official-schema publication is documented as future asset intake and must not keep connector tests pending in this sprint.
Report Types
| Report type | Wire value | Intended cadence |
|---|---|---|
| Early warning | early_warning_24h | 24 hours |
| Vulnerability notification | vulnerability_notification_72h | 72 hours |
| Final report | final_report_14d | 14 days |
The shared CRA timeline service owns cadence opening, alerts, and final-report transmission hash carry-forward. This connector validates the due-time fields it receives and requires previous transmission hashes on final reports.
Exploitation Status
incident.exploitationStatus is a closed enum (EnisaExploitationStatus) emitted with these wire values:
| Enum member | Wire value |
|---|---|
ActivelyExploited | actively_exploited |
SuspectedExploitation | suspected_exploitation |
SevereIncident | severe_incident |
Mitigated | mitigated |
A value outside this set fails local validation (incident.exploitationStatus is not supported.).
Envelope Shape
The local envelope is compact JSON (written unindented by the encoder) with the deterministic property order shown below. The example is a vulnerability_notification_72h report, so it carries at least one vulnerability and one mitigation alongside the always-required asset, attestation, and evidence references:
{
"contractVersion": "enisa-incident-reporting.v1",
"localEnvelopeSchemaVersion": "enisa-incident-reporting-local-envelope-v1",
"enisaSrpSchemaVersion": "BLOCKED-no-local-enisa-srp-schema",
"reportType": "vulnerability_notification_72h",
"reportId": "enisa-report-20260430-001",
"tenantId": "tenant-a",
"channelId": "chn-enisa-cra",
"generatedAt": "2026-04-30T10:00:00.000Z",
"operatorApprovalRef": "approval://tenant-a/enisa/incident-cra-001/vulnerability-notification",
"reporter": {
"legalEntityName": "Example Manufacturer GmbH",
"countryCode": "DE",
"contactRef": "authority://tenant/tenant-a/contacts/security",
"complianceProfileRef": "authority://tenant/tenant-a/compliance-profile/v1"
},
"productInstance": {
"productId": "stella-ops-suite",
"productName": "Stella Ops Suite",
"productVersion": "2026.04",
"productInstanceId": "release://stella/2026.04/prod-eu",
"deploymentEnvironment": "production",
"assetRefs": ["asset://tenant-a/api-gateway/prod-eu"]
},
"incident": {
"incidentId": "incident-cra-001",
"title": "Actively exploited API deserialization vulnerability",
"summary": "Exploit attempts against the public API were confirmed.",
"severity": "critical",
"status": "mitigating",
"exploitationStatus": "actively_exploited",
"startedAt": "2026-04-30T07:15:00.000Z",
"detectedAt": "2026-04-30T07:45:00.000Z",
"closedAt": null,
"impactSummary": "No confirmed data loss.",
"rootCauseSummary": null,
"remediationSummary": null
},
"timeline": {
"timelineRef": "ledger://notify/cra/timeline/incident-cra-001",
"openedAt": "2026-04-30T08:00:00.000Z",
"earlyWarningDueAt": "2026-05-01T08:00:00.000Z",
"vulnerabilityNotificationDueAt": "2026-05-03T08:00:00.000Z",
"finalReportDueAt": "2026-05-14T08:00:00.000Z",
"previousTransmissionHashes": []
},
"vulnerabilities": [
{
"vulnerabilityId": "vuln-cra-001",
"cveId": "CVE-2026-0001",
"advisoryId": "advisory://tenant-a/cra-001",
"affectedVersionRange": "<2026.04",
"fixedVersion": "2026.04"
}
],
"mitigations": [
{
"mitigationId": "mit-cra-001",
"description": "Disabled the affected deserialization endpoint and rotated keys.",
"status": "applied",
"publishedAt": "2026-04-30T09:30:00.000Z"
}
],
"attestationRefs": ["attest://tenant-a/cra-001/dsse"],
"evidenceRefs": ["evidence://tenant-a/cra-001/packet"],
"localValidation": {
"status": "passed",
"ruleSetVersion": "enisa-incident-reporting-local-validation-v1"
},
"enisaSrpSchemaValidation": {
"status": "blocked",
"reason": "No pinned ENISA SRP schema package is present in the local repository scope; external schema fetches are disabled for this sprint.",
"evidence": []
}
}
Optional object fields are written explicitly as JSON null when absent (see incident.closedAt, incident.rootCauseSummary, incident.remediationSummary above), not omitted. Each vulnerability entry may carry optional cveId, advisoryId, affectedVersionRange, and fixedVersion; each mitigation may carry an optional publishedAt. The enisaSrpSchemaValidation.evidence array is populated from the encoder’s blocked-schema evidence list (the example shows it empty for brevity).
Required Fields
Every report requires:
- tenant id, report id, channel id, generated UTC timestamp, and operator approval reference
- channel purpose
enisa-cra - reporter legal entity name, ISO 3166-1 alpha-2 country code, contact reference, and compliance profile reference
- product id, product name, product version, product instance id, deployment environment, and at least one asset reference
- incident id, title, summary, severity, status, exploitation status, and start time
- timeline reference and 24h / 72h / 14d due timestamps
- at least one attestation reference and one immutable evidence reference
The 72h and final report types additionally require at least one vulnerability reference and one mitigation. Final reports require closedAt, rootCauseSummary, remediationSummary, and at least one previousTransmissionHashes entry.
Local Validation
The encoder fails closed before output when:
- contract or schema pin values do not match v1 constants (
contractVersion,localEnvelopeSchemaVersion,enisaSrpSchemaVersion) - the channel is missing, the channel tenant does not match
tenantId, or the channel purpose is notenisa-cra - timestamps are missing or timeline due times are out of order (
earlyWarningDueAt<openedAt,vulnerabilityNotificationDueAt<earlyWarningDueAt, orfinalReportDueAt<vulnerabilityNotificationDueAt) incident.detectedAtorincident.closedAt, when present, are earlier thanincident.startedAtincident.exploitationStatusis not one of the four defined enum values- the country code is not ISO 3166-1 alpha-2 (
^[A-Z]{2}$) - product asset references are empty, or attestation/evidence references are empty (at least one of each is always required)
- final reports omit earlier transmission hashes
- final reports omit closure, root-cause, or remediation fields
- 72h/final reports omit vulnerability or mitigation data
- SHA-256 hashes are malformed (must match
^sha256:[0-9a-f]{64}$) - the generated envelope hash does not match its UTF-8 content, or the re-parsed envelope’s
contractVersion,localEnvelopeSchemaVersion,reportId,incident.incidentId, orenisaSrpSchemaValidation.statusdo not match the package
Validation failures raise EnisaIncidentReportValidationException carrying the full list of error messages. Normalization runs before validation: required strings are trimmed, country codes are upper-cased, severity/status/deployment environment are lower-cased, CVE ids are upper-cased, and timestamps are converted to UTC.
Signing
EnisaSignerPipelineSigner (implementing IEnisaIncidentReportSigner) adapts the package to the existing ISignerPipeline. The signing subject is the generated envelope file name plus its SHA-256 digest (carried in the subject’s sha256 attribute as 64 lowercase hex characters, without the sha256: prefix). The predicate type is https://stellaops.org/predicates/enisa-incident-report/v1; the predicate body is the local envelope JSON. The request uses signing mode Kms with DSSE output, and the caller context binds the package tenant, the supplied subject, scopes, and audiences. The SignerPipeline emits a DSSE envelope with payload type application/vnd.in-toto+json.
The returned EnisaSignedReport carries the serialized DSSE envelope JSON, its SHA-256 digest, the signed subject digest, the payload type, the signature profile signer-pipeline-in-toto, and the optional signer audit id and key id.
Adjacent tests also use a deterministic local HMAC signer to prove offline verification mechanics without requiring live KMS, ENISA, or web access.
Delivery
Default delivery mode is operator-filesystem.
Channel configuration:
| Key | Required | Meaning |
|---|---|---|
target or properties.dropDirectory | Default mode | Directory where the handoff bundle is written |
properties.autoSubmit or properties.notify.channel.enisa.autoSubmit | Optional | Must be literal true to request auto-submit |
endpoint, properties.srpEndpoint, or properties.notify.channel.enisa.srpEndpoint | Auto-submit | Tenant-configured SRP endpoint |
properties.tenantApproverRef or properties.notify.channel.enisa.tenantApproverRef | Auto-submit | Customer/operator approver evidence |
properties.freshAuthRef or properties.notify.channel.enisa.freshAuthRef | Auto-submit | Fresh authentication evidence |
Each channel config key is read first under its bare name in channel.config.properties (for example dropDirectory) and then under the notify.channel.enisa.-prefixed name (for example notify.channel.enisa.dropDirectory). dropDirectory also falls back to the channel target, and srpEndpoint falls back to the channel endpoint. autoSubmit must be the literal string true (case-insensitive) to enable auto-submit; any other value (or absence) selects the default operator-filesystem mode.
When auto-submit is requested without all three auto-submit fields (srpEndpoint, tenantApproverRef, freshAuthRef), the connector fails closed. The connector records an audit-grade delivery event in the delivery ledger metadata: enisa.auto_submit.requested for the auto-submit path and enisa.operator_filesystem_drop.prepared for the default path. The delivery-ledger entry is a NotifyDelivery with rule id enisa-cra, kind enisa-cra-report, status Queued, channel type Custom, and format Json; its metadata carries enisa.* keys (contract version, delivery mode, report type/id, envelope and DSSE SHA-256 digests, signer audit id, signature profile, SRP schema validation status, and hashed signer key id / fresh-auth reference). The fresh-auth reference is stored only as a SHA-256 hash, never in plaintext.
Auto-Submit HTTP Transport
EnisaSrpHttpTransport.DeliverSignedAsync performs the real auto-submit transport. It calls EnisaIncidentReportExportService.ExportSignedAsync (produce-and-sign default) and then, when autoSubmit is enabled, POSTs the signed envelope to the configured SRP endpoint. The submission body (schemaVersion = enisa-srp-submission-v1, media type application/vnd.stellaops.enisa-incident-reporting.srp-submission.v1+json) carries the pinned schema-version metadata, the produce-and-sign envelope, and its DSSE signature; request headers include the report id and the envelope / DSSE / body SHA-256 digests and the pinned SRP schema version.
The transport fails closed before transmit when any of these gates fail, falling back to the operator filesystem drop (the report is never silently lost) and recording the reason in the delivery ledger:
- channel disabled (
channel-disabled) - target not approved — tenant approver reference absent (
target-not-approved) - trust material not resolvable via the crypto provider registry (
trust-material-missing) - SRP endpoint not a well-formed absolute URI (
invalid-srp-endpoint)
On an HTTP non-2xx response (srp-returned-http-<code>) or a transport exception (transport-failed:… / transport-timeout:…) it also falls back to the filesystem drop and records the failure. On success it records a Sent delivery with enisa.srp.transport = auto-submit-http, enisa.srp.receiptStatus = accepted, the HTTP status, the hashed SRP endpoint, and the submission body SHA-256.
The signing key is resolved through IEnisaSrpTrustMaterial — EnisaSrpCryptoTrustMaterial resolves it via ICryptoProviderRegistry (EnisaSrpTransportOptions.KeyId is a logical handle into the active crypto provider, never raw key material) so regional crypto profiles (EIDAS / FIPS / GOST / SM / HSM) apply. The transport never derives keys from config strings. Persistence is via the IEnisaReportDeliveryLedger seam (mirrors IDoraInfoSharingDeliveryLedger); the host stores the NotifyDelivery row. Tests exercise the transport against an in-process handler and a live loopback stand-in SRP receiver — no live ENISA endpoint is contacted at test time.
Filesystem handoff writes the bundle under <dropDirectory>/<tenantId>/<reportId>/ (tenant and report id are sanitized: path separators, :, and invalid filename characters become _). The <report-segment> is the hyphenated report-type slug — early-warning-24h, vulnerability-notification-72h, or final-report-14d:
<report-segment>.enisa-envelope.json<report-segment>.enisa-envelope.json.dsse.jsonnotify-delivery-ledger-event.jsonaudit-event.jsonmanifest.json
All files are written UTF-8. manifest.json records the contract version, report/tenant ids, delivery mode, envelope and DSSE file names + SHA-256 digests, and the SRP schema validation status.
Channel Health And Test Preview
The connector registers two Notify channel-framework providers (both bound for channel type Custom via [ServiceBinding]):
EnisaChannelHealthProviderreportsHealthywhen the channel purpose isenisa-cra, the channel is enabled, and delivery configuration is complete;Unhealthywhen required config is missing; andDegradedwhen the channel is disabled or not configured for ENISA CRA reporting. Health metadata includes the resolved delivery mode, auto-submit flag, hashed secret/endpoint references, and any validation errors.EnisaChannelTestProviderbuilds a non-signed JSON preview body (contract and schema versions, purpose, report type, delivery mode, auto-submit flag, and the blocked SRP schema-validation status). The preview defaults the report type toearly_warning_24hunless areportTypeis supplied in the test request metadata. The preview does not produce a signed envelope or write files.
Determinism And Offline Replay
- No wall-clock time, random ids, live web fetches, or live ENISA endpoints are used by the encoder.
- Inputs use caller-supplied UTC timestamps and stable ids.
- Asset refs, vulnerability refs, mitigation refs, evidence refs, and attestation refs are sorted ordinally before encoding.
- Dates are formatted as
yyyy-MM-ddTHH:mm:ss.fffZ. - SHA-256 values are lowercase and prefixed with
sha256:. - Adjacent tests assert deterministic envelope output, offline signature verification, guarded auto-submit, and filesystem drop artifacts.
- The auto-submit HTTP transport is tested against an in-process handler and a live loopback stand-in SRP receiver that verifies the DSSE envelope offline; no external network or live ENISA endpoint is contacted at test time.
