NIS2 Report
NIS2 incident reporting on the clocks the directive sets: an early warning within 24 hours of becoming aware, a notification within 72, and a final report within one month. Those deadlines are fixed here rather than configured, because they are the directive's and not the deployment's.
An entity is registered with its NIS2 identifier, its Article 3 classification as essential or important, and its sector annex, since a CSIRT submission is rejected without them. Each stage records what is known at that point and can submit the envelope to the CSIRT endpoint in the same call.
The filing and the submission are two things. The stage is recorded against the directive's deadline first, and the POST to the CSIRT runs after it, so a stage can be filed on time while the endpoint is down. The call reports that as submitted.submitted === false with the status code or the error, and a stage is filed once: calling the stage again raises incident-report/stage-already-filed. Send it again with resubmit(incidentId, stage), which re-POSTs an already-filed stage without touching the deadline record.
The clock runs from detectedAt, when the entity became aware, rather than from when the incident was entered, which is what the directive's "within 24 hours of becoming aware" means. status() answers across the register rather than per incident: how many are open, how many closed, and how many have passed each of the three deadlines. That is the number an operator reports upward; which stage one incident owes next comes from get(id).
b.nis2.report.create(opts) #
{
entityId: string, // required; the NIS2 registration identifier
entityType: string, // required; "essential" or "important"
sectorAnnex: string, // required; e.g. "I.6", "II.6"
csirtEndpoint: string, // where a submitted envelope is sent
httpClient: object, // client used for the submission
persist: object, // store for the incident records
now: object, // function returning epoch ms; for tests
audit: object, // audit instance, or false to stay silent
}
Open NIS2 reporting for one registered entity. entityId, entityType and sectorAnnex are required, raising nis2-report/bad-entity-id, nis2-report/bad-entity-type and nis2-report/bad-sector. An entityType outside essential and important is refused, since Article 3 admits no third class.
The returned reporter answers open, earlyWarning, notification, finalReport and resubmit, plus get, list and status. Each stage records the fields known at that point, and passing submit: true also sends the envelope to csirtEndpoint through httpClient, answering with both the stored record and the submitted result.
submitted is null when the call did not ask to submit, and otherwise { submitted, statusCode } or { submitted, error }. A stage filed while the endpoint was unreachable therefore reports submitted: false and stays filed, since the filing is what the directive's clock measures. Send it again with resubmit(incidentId, stage), where stage is early-warning, notification or final: it re-POSTs the envelope for an already-filed stage and leaves the deadline record untouched. It sends the fields as the stage record holds them, which is a structured copy of what was filed, so pass plain data to a stage if a retry has to render identically. A third argument replaces them for a report corrected in the meantime. A stage outside those three raises nis2-report/bad-stage, an unknown incident nis2-report/unknown-incident, and a stage with no filing yet nis2-report/stage-not-filed, since there would be nothing to send.
The deadlines come from the directive and are not options: 24 hours for the early warning, 72 for the notification and 30 days for the final report, all measured from detectedAt, which open requires and does not default.
var b = require("@blamejs/core");
var nis2 = b.nis2.report.create({
entityId: "DE-1234",
entityType: "essential",
sectorAnnex: "I.6",
});
var opened = await nis2.open({
detectedAt: Date.now(), // required; every deadline runs from here
scope: "billing-api",
summary: "Unauthorized access detected",
impact: "confidentiality",
});
await nis2.earlyWarning(opened.id, { significant: true });
nis2.status(); // → { total, open, closed, late: { … } }