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:


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:

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

(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 typeWire valueIntended cadence
Early warningearly_warning_24h24 hours
Vulnerability notificationvulnerability_notification_72h72 hours
Final reportfinal_report_14d14 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 memberWire value
ActivelyExploitedactively_exploited
SuspectedExploitationsuspected_exploitation
SevereIncidentsevere_incident
Mitigatedmitigated

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:

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:

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:

KeyRequiredMeaning
target or properties.dropDirectoryDefault modeDirectory where the handoff bundle is written
properties.autoSubmit or properties.notify.channel.enisa.autoSubmitOptionalMust be literal true to request auto-submit
endpoint, properties.srpEndpoint, or properties.notify.channel.enisa.srpEndpointAuto-submitTenant-configured SRP endpoint
properties.tenantApproverRef or properties.notify.channel.enisa.tenantApproverRefAuto-submitCustomer/operator approver evidence
properties.freshAuthRef or properties.notify.channel.enisa.freshAuthRefAuto-submitFresh 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:

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 IEnisaSrpTrustMaterialEnisaSrpCryptoTrustMaterial 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:

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]):

Determinism And Offline Replay