Assurance Reporting Timeline v1
Contract ID: CONTRACT-ASSURANCE-REPORTING-TIMELINE-V1-004 Status: Implemented Owner: Notify Guild Schema version: assurance-reporting-timeline-v1 Readiness schema version: assurance-reporting-readiness-v1
This contract describes the regulatory reporting-milestone profiles for Stella Ops Assurance packs — how each framework’s deadline-driven incident reporting (NIS2 Article 23, DORA Article 19, CRA Article 14) maps onto Notify’s single shared timeline service. It is for Notify integrators, Web/CLI authors rendering reporting deadlines, and reviewers verifying that handoff and auto-submit posture is gated correctly. It is the reporting-timeline member of the Assurance contract family (assurance-pack-v1is the catalogue entry point).
Purpose
assurance-reporting-timeline-v1 describes reporting milestone profiles for Assurance packs. Notify owns one shared reporting timeline service. NIS2, DORA, CRA, and future packs register milestone profiles over that service instead of creating duplicate state machines.
Profile Fields
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion | string | yes | MUST be assurance-reporting-timeline-v1. |
timelineProfileId | string | yes | Stable profile id. |
frameworkId | string | yes | Framework id such as nis2, dora, or cra. |
packId | string | yes | Pack id from assurance-pack-v1. |
reportType | string | yes | Incident, vulnerability, advisory, or other report type. |
claimBoundary | string | yes | Claim boundary from assurance-pack-v1. |
runtimeRegimeId | string | yes | Notify IncidentReportTimelineService regime id backing this profile. |
timelineSource | string | yes | Runtime service name. Current value is IncidentReportTimelineService. |
milestones[] | array | yes | Ordered milestone definitions. |
deadlinePolicy | object | yes | Rules for deadline calculation. |
payloadContract | string | yes | Pack-specific payload contract. |
signingRequired | boolean | yes | Whether transmission/handoff payloads must be signed. |
operatorApproval | object | yes | Approval and auto-submit posture. |
channels[] | array | yes | Supported channels or channel families. |
compatibilityRoutes[] | string array | no | Existing routes or commands. Always includes the shared GET /api/v1/regulatory/reporting-timelines?regime=<regimeId> route for the profile’s runtime regime. |
The list response wraps the profiles in an envelope:
{
"schemaVersion": "assurance-reporting-timeline-v1",
"items": [ /* AssuranceReportingTimelineProfile[] */ ],
"sourceOfTruth": "Notify"
}
All three profiles (nis2.article23.incident, dora.article19.incident, cra.article14.incident) are built deterministically at service startup from the Notify IncidentReportTimelineService regimes by AssuranceReportingProfileRegistry and held in an immutable sorted map. Profiles are not tenant-scoped or persisted; only readiness (below) is tenant-aware.
Milestone Shape
{
"milestoneId": "early-warning",
"displayName": "Early warning",
"timelineMilestoneName": "early_warning",
"transmissionKind": "early_warning",
"dueWithin": "PT24H",
"stateWhenComplete": "early_warning_sent",
"payloadRequired": true,
"signatureRequired": true,
"ledgerEventKind": "nis2.incident.reported",
"requiredEvidence": ["incident_opened_event_ref", "impact_summary_ref"]
}
dueWithin uses ISO 8601 duration syntax. Deadline calculation must use the timeline’s recorded opening time and deterministic clock inputs, not local wall-clock reads during replay.
Operator Approval
{
"defaultPosture": "produce-and-sign-handoff",
"autoSubmitAvailable": true,
"autoSubmitDefault": false,
"freshAuthRequired": true,
"approverAction": "submit"
}
Auto-submit must be opt-in per tenant/channel and audit logged. The default posture for regulator-facing flows is produce-and-sign handoff.
Example: NIS2 Article 23
{
"schemaVersion": "assurance-reporting-timeline-v1",
"timelineProfileId": "nis2.article23.incident",
"frameworkId": "nis2",
"packId": "nis2",
"reportType": "incident",
"claimBoundary": "operator-support",
"runtimeRegimeId": "nis2",
"timelineSource": "IncidentReportTimelineService",
"milestones": [
{ "milestoneId": "early-warning", "displayName": "Early warning", "timelineMilestoneName": "early_warning", "transmissionKind": "early_warning", "dueWithin": "PT24H", "stateWhenComplete": "early_warning_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "nis2.incident.reported", "requiredEvidence": ["incident_opened_event_ref", "impact_summary_ref"] },
{ "milestoneId": "notification", "displayName": "Incident notification", "timelineMilestoneName": "notification", "transmissionKind": "notification", "dueWithin": "PT72H", "stateWhenComplete": "notification_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "nis2.incident.reported", "requiredEvidence": ["incident_classified_event_ref", "operator_approval_ref", "severity_classification_ref"] },
{ "milestoneId": "final-report", "displayName": "Final report", "timelineMilestoneName": "final_report", "transmissionKind": "final_report", "dueWithin": "P1M", "stateWhenComplete": "final_report_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "nis2.incident.reported", "requiredEvidence": ["early_warning_payload_hash", "final_report_bundle_ref", "notification_payload_hash", "operator_approval_ref"] }
],
"deadlinePolicy": { "startEvent": "incident.opened", "timelineOpenedEventKind": "nis2.incident.opened", "timezone": "UTC", "replaySource": "timeline_opened_at" },
"payloadContract": "nis2-incident-report-envelope-v1",
"signingRequired": true,
"operatorApproval": {
"defaultPosture": "produce-and-sign-handoff",
"autoSubmitAvailable": true,
"autoSubmitDefault": false,
"freshAuthRequired": true,
"approverAction": "submit"
},
"channels": ["nis2-csirt.de", "nis2-csirt.fr", "nis2-csirt.nl", "signed-json-webhook", "pgp-email"],
"compatibilityRoutes": ["/compliance/nis2/incidents", "/api/v1/regulatory/reporting-timelines?regime=nis2"]
}
Example: CRA Article 14
{
"schemaVersion": "assurance-reporting-timeline-v1",
"timelineProfileId": "cra.article14.incident",
"frameworkId": "cra",
"packId": "cra.product-security",
"reportType": "incident-or-actively-exploited-vulnerability",
"claimBoundary": "manufacturer-self",
"runtimeRegimeId": "cra",
"timelineSource": "IncidentReportTimelineService",
"milestones": [
{ "milestoneId": "early-warning", "displayName": "Early warning", "timelineMilestoneName": "early_warning", "transmissionKind": "early_warning", "dueWithin": "PT24H", "stateWhenComplete": "early_warning_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "cra.incident.reported", "requiredEvidence": ["exploitation_evidence_ref", "incident_classified_event_ref"] },
{ "milestoneId": "notification", "displayName": "Vulnerability notification", "timelineMilestoneName": "vulnerability_notification", "transmissionKind": "vulnerability_notification", "dueWithin": "PT72H", "stateWhenComplete": "notification_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "cra.incident.reported", "requiredEvidence": ["incident_classified_event_ref", "operator_approval_ref", "vulnerability_notification_bundle_ref"] },
{ "milestoneId": "final-report", "displayName": "Final report", "timelineMilestoneName": "final_report", "transmissionKind": "final_report", "dueWithin": "P14D", "stateWhenComplete": "final_report_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "cra.incident.reported", "requiredEvidence": ["early_warning_payload_hash", "final_report_bundle_ref", "operator_approval_ref", "vulnerability_notification_payload_hash"] }
],
"deadlinePolicy": { "startEvent": "incident.classified", "timelineOpenedEventKind": "cra.incident.classified", "timezone": "UTC", "replaySource": "timeline_opened_at" },
"payloadContract": "enisa-incident-reporting-v1",
"signingRequired": true,
"operatorApproval": {
"defaultPosture": "produce-and-sign-handoff",
"autoSubmitAvailable": true,
"autoSubmitDefault": false,
"freshAuthRequired": true,
"approverAction": "submit"
},
"channels": ["enisa-cra", "signed-json-webhook", "pgp-email"],
"compatibilityRoutes": ["/api/v1/regulatory/reporting-timelines?regime=cra"]
}
Example: DORA Major ICT Incident
dora.article19.incident is registered by AssuranceReportingProfileRegistry (method BuildDoraProfile) alongside the NIS2 and CRA profiles, so it is returned from GET /api/v1/assurance/reporting-timeline-profiles?frameworkId=dora and exposes readiness like the other profiles. DORA timeline records also remain available through the shared regulatory timeline route with regime=dora.
{
"schemaVersion": "assurance-reporting-timeline-v1",
"timelineProfileId": "dora.article19.incident",
"frameworkId": "dora",
"packId": "dora",
"reportType": "major-ict-incident",
"claimBoundary": "operator-support",
"runtimeRegimeId": "dora",
"timelineSource": "IncidentReportTimelineService",
"milestones": [
{ "milestoneId": "initial-report", "displayName": "Initial report", "timelineMilestoneName": "initial_report", "transmissionKind": "initial_report", "dueWithin": "PT4H", "stateWhenComplete": "early_warning_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "dora.incident.reported", "requiredEvidence": ["dora_major_incident_classification_ref", "incident_opened_event_ref"] },
{ "milestoneId": "intermediate-report", "displayName": "Intermediate report", "timelineMilestoneName": "intermediate_report", "transmissionKind": "intermediate_report", "dueWithin": "PT72H", "stateWhenComplete": "notification_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "dora.incident.reported", "requiredEvidence": ["incident_classified_event_ref", "intermediate_report_bundle_ref", "operator_approval_ref"] },
{ "milestoneId": "final-report", "displayName": "Final report", "timelineMilestoneName": "final_report", "transmissionKind": "final_report", "dueWithin": "P1M", "stateWhenComplete": "final_report_sent", "payloadRequired": true, "signatureRequired": true, "ledgerEventKind": "dora.incident.reported", "requiredEvidence": ["final_report_bundle_ref", "initial_report_payload_hash", "intermediate_report_payload_hash", "operator_approval_ref"] }
],
"deadlinePolicy": { "startEvent": "incident.opened", "timelineOpenedEventKind": "dora.incident.opened", "timezone": "UTC", "replaySource": "timeline_opened_at" },
"payloadContract": "dora-major-ict-incident-report-v1",
"signingRequired": true,
"operatorApproval": {
"defaultPosture": "produce-and-sign-handoff",
"autoSubmitAvailable": true,
"autoSubmitDefault": false,
"freshAuthRequired": true,
"approverAction": "submit"
},
"channels": ["dora-competent-authority-handoff", "signed-json-webhook", "pgp-email"],
"compatibilityRoutes": [
"/docs/compliance/packs/dora/incident-reporting.md",
"/api/v1/regulatory/reporting-timelines?regime=dora"
]
}
The DORA signed-handoff bundle is also produced and verified offline via the CLI stella export dora-incident-report and stella verify dora-incident-report commands (DoraIncidentCommandGroup), which consume a dora-major-incident-report.v1 input JSON. These CLI commands are not listed in the profile’s compatibilityRoutes array.
Runtime API
Notify exposes profile metadata through these endpoints. The read-only profile policy accepts any of notify.viewer, policy:read, or policy:audit:
GET /api/v1/assurance/reporting-timeline-profilesGET /api/v1/assurance/reporting-timeline-profiles?frameworkId=nis2GET /api/v1/assurance/reporting-timeline-profiles?frameworkId=doraGET /api/v1/assurance/reporting-timeline-profiles?reportType=incidentGET /api/v1/assurance/reporting-timeline-profiles/{timelineProfileId}GET /api/v1/assurance/reporting-timeline-profiles/{timelineProfileId}/readiness
The list endpoint accepts two optional, case-insensitive query filters, frameworkId and reportType; both are normalised (trimmed, lower-cased) and applied with ordinal equality. Results are ordered by frameworkId, then reportType, then timelineProfileId. Unknown timelineProfileId values return HTTP 404 with error code profile_not_found. The readiness endpoint also requires a tenant claim and returns HTTP 400 (tenant_missing) if it is absent.
The existing runtime timeline records remain at GET /api/v1/regulatory/reporting-timelines (schema eu-reporting-timeline.v1, also notify.viewer-scoped, supporting regime, incidentId, evidenceRef, state, active, limit, and cursor query parameters). Profiles only describe how a pack maps onto the shared Notify timeline; they do not create a second persistence model.
Readiness responses use schema assurance-reporting-readiness-v1 and include separate operatorHandoffReady and autoSubmitReady booleans. The full payload shape is:
{
"schemaVersion": "assurance-reporting-readiness-v1",
"timelineProfileId": "nis2.article23.incident",
"frameworkId": "nis2",
"packId": "nis2",
"reportType": "incident",
"claimBoundary": "operator-support",
"tenantId": "tenant-a",
"status": "configured",
"operatorHandoffReady": true,
"autoSubmitReady": false,
"reasonCodes": ["auto-submit-disabled"],
"operatorHandoffReasonCodes": ["configured"],
"autoSubmitReasonCodes": ["auto-submit-disabled"],
"secretValuesExposed": false,
"updatedAt": "2026-01-01T00:00:00Z"
}
status is configured when operatorHandoffReady is true, otherwise blocked. reasonCodes is the deduplicated, ordinally sorted union of the handoff and auto-submit reasons (defaulting to ["configured"] when both lanes are clear). The per-lane operatorHandoffReasonCodes and autoSubmitReasonCodes arrays each fall back to ["configured"] when their lane has no blockers. tenantId is echoed from the resolved tenant claim. Reason codes are stable:
| Reason code | Meaning | Setup owner |
|---|---|---|
configured | The checked readiness lane has no missing inputs. | Notify |
csirt-destination-missing | No NIS2 CSIRT/NCA destination is configured for a CSIRT channel profile. | Tenant/operator setting |
pgp-recipient-key-missing | The public recipient key for encrypted email handoff is missing. | Tenant/operator setting |
webhook-signing-key-missing | The sealed signing key reference for signed webhook handoff is missing. | Environment/secret configuration |
operator-approval-missing | Fresh operator approval is not configured or present. | Authority operator approval |
auto-submit-disabled | Auto-submit is available but intentionally disabled for the tenant/channel. | Tenant/operator setting |
publication-handoff-not-configured | Local handoff/output publication is not configured. | Environment or tenant setup, depending on deployment |
Readiness is computed from per-profile boolean flags bound from configuration section notify:assuranceReporting (class AssuranceReportingOptions). Each profile id maps to an AssuranceReportingProfileReadinessOptions block keyed under Profiles; absent profiles default every flag to false (the most blocked state). The reason codes are derived as follows:
| Config flag | When false, emits reason | Applies when |
|---|---|---|
CsirtDestinationConfigured | csirt-destination-missing | Profile has any nis2-csirt.* channel |
PgpRecipientKeyConfigured | pgp-recipient-key-missing | Profile has the pgp-email channel |
WebhookSigningKeyConfigured | webhook-signing-key-missing | Profile has the signed-json-webhook channel |
OperatorApprovalConfigured | operator-approval-missing | Profile’s operatorApproval.freshAuthRequired is true |
PublicationHandoffConfigured | publication-handoff-not-configured | Always evaluated |
AutoSubmitEnabled | auto-submit-disabled (auto-submit lane only) | Profile’s operatorApproval.autoSubmitAvailable is true |
The auto-submit lane inherits every operator-handoff reason and additionally adds auto-submit-disabled, so autoSubmitReady can never be true unless operatorHandoffReady is also true.
Readiness payloads must set secretValuesExposed=false and must not include secret values, raw keys, endpoint credentials, storage roots, or provider tokens.
Runtime Record Fields
Runtime timeline records are returned by GET /api/v1/regulatory/reporting-timelines as ReportingTimelineItem entries (schema eu-reporting-timeline.v1). Each item carries:
regime— runtime regime id (nis2,dora,cra); maps to the profile’sruntimeRegimeIdincidentIdstate— uppercased:OPEN,EARLY_WARNING_SENT,NOTIFICATION_SENT,FINAL_REPORT_SENT,CLOSEDopenedAt,closedAtmilestone— name of the current pending milestone (null when completed)deadline,deadlineStatus(PENDING,APPROACHING,MISSED,COMPLETED)payloadHash,signerRefreplayHash— deterministicsha256:digest of the recorded timeline factsevidenceRefs[],ledgerRefs[],auditEventIds[]milestones[]— each withname,transmissionKind,deadline,deadlineStatus,completedAt,transmissionId,payloadHash,signerRef,ledgerEventKind,requiredEvidence[]transmissions[]— each withtransmissionId,milestone,transmissionKind,ledgerEventKind,sentAt,payloadHash,signerRef,evidenceRefs{},sourceEventRef,hlc, anddelivery(nullable) — the CSIRT/ENISA delivery result for the transmission, an object{ mode, status, recordedAt, detail }wheremodeis the authority-target intake mode,statusis the recorded delivery status,recordedAtis the delivery timestamp, anddetail(nullable) is an optional human-readable note;deliveryisnullwhen the transmission was recorded without a delivery outcomealerts[]— each withseverity(SEV-1),kind(deadline_75_percent_elapsed,missed_deadline),milestone,deadline,observedAt,elapsedPercentupdatedAt
The list response is wrapped in { schemaVersion, items[], cursor, sourceOfTruth: "Notify" }, where cursor is a base64url-encoded offset for pagination (default page size 100, maximum 500).
Determinism Rules
- Deadline calculations use UTC and recorded timeline facts.
- Runtime milestones are sorted by deadline, then milestone name (ordinal); pending transmissions and alerts follow the same order. Profile milestone arrays preserve registry declaration order.
- Payload hashes use lower-case SHA-256, validated as 64 hex chars or
sha256:<hex>and normalised to thesha256:prefix (NormalizeSha256). - The
replayHashis a deterministicsha256:digest over the recorded timeline facts (ComputeReplayHash); transmission ids areirtx_<24 hex>derived from regime, tenant, incident, milestone, sent-at, and payload hash. - Replay must not require a live regulator endpoint.
Implementation Grounding
- Endpoints:
AssuranceReportingProfileEndpointsandReportingTimelineEndpointsinsrc/Notify/StellaOps.Notify.WebService/Endpoints/. - Profile registry and DTOs:
AssuranceReportingProfileRegistry(SchemaVersion = "assurance-reporting-timeline-v1",ReadinessSchemaVersion = "assurance-reporting-readiness-v1") insrc/Notify/StellaOps.Notify.WebService/Services/. - Regime definitions (milestones, event kinds, evidence, states):
IncidentReportTimelineService. - Readiness configuration:
AssuranceReportingOptions(sectionnotify:assuranceReporting). - DI wiring and
notify.viewerpolicy:src/Notify/StellaOps.Notify.WebService/Program.cs.
