NIS2 Article 23 gives you a 24-hour early warning, a 72-hour incident notification and a one-month final report, and those deadlines are not negotiable. The practical problem is not the reporting template but the clock: the 24 hours run from awareness, which means your instance has to be able to tell you when awareness began. This article implements the workflow in ServiceNow so the timestamps that prove compliance are produced by the process rather than reconstructed afterwards.
What This Is
NIS2 Article 23 imposes a mandatory, multi-stage incident notification process on all essential and important entities. When a "significant incident" occurs, organizations must notify their national CSIRT (Computer Security Incident Response Team) or competent authority within strict, non-negotiable deadlines. Missing any deadline triggers supervisory action and potential fines up to EUR 10 million or 2% of global annual turnover.
This article covers the full implementation in ServiceNow: from classifying whether an incident triggers Art.23 reporting, to automated SLA timers, to generating the actual notification content at each stage, to maintaining a complete evidence trail for regulatory proof.
The Three-Stage Reporting Timeline
HOUR 0: Significant incident detected or identified
│
│ ┌──────────────────────────────────────────────────────────────────────┐
│ │ CLOCK STARTS when the entity becomes "aware" of the incident. │
│ │ "Aware" = when the organization has reasonable certainty that a │
│ │ significant incident has occurred. This is NOT when the root cause │
│ │ is confirmed — it is when the symptoms are recognised as meeting │
│ │ the Art.23§3 significance criteria. │
│ └──────────────────────────────────────────────────────────────────────┘
│
▼
STAGE 1 — EARLY WARNING (24 hours) [Art.23§4(a)]
│ Submit to national CSIRT / competent authority:
│ • Is the incident suspected to be caused by unlawful or malicious acts?
│ • Could the incident have cross-border impact?
│ • Brief description of what happened
│
▼
STAGE 2 — INCIDENT NOTIFICATION (72 hours) [Art.23§4(b)]
│ Update the early warning with:
│ • Initial assessment of severity and impact
│ • Indicators of compromise (where available)
│ • Affected services and systems
│ • Estimated number of affected users/entities
│
▼
STAGE 3 — FINAL REPORT (1 month) [Art.23§4(d)]
Submit comprehensive report:
• Detailed description of the incident and its root cause
• Type of threat or root cause that triggered it
• Mitigation measures applied and ongoing
• Cross-border impact (if any)
• Lessons learned and control improvements
If the incident is STILL ONGOING at the 1-month mark:
Submit a PROGRESS REPORT instead [Art.23§4(e)], then submit the
final report within 1 month after the incident is resolved.
What Is a "Significant Incident" Under Art.23§3?
An incident is "significant" if it meets any of the following criteria defined in Art.23§3:
| Criterion | Definition | ServiceNow Context |
|---|---|---|
| Severe operational disruption | The incident has caused or is capable of causing severe operational disruption of the services or financial loss for the entity | ServiceNow platform outage affecting ITSM, CMDB, or SecOps operations; loss of critical workflow automation |
| Affected other persons | The incident has affected or is capable of affecting other natural or legal persons by causing considerable material or non-material damage | Data breach exposing user PII (sys_user), customer records, HR data; credential theft affecting downstream systems |
| Cross-border impact | The incident has caused or could cause impact in other Member States | Multi-national ServiceNow deployments serving users in multiple EU countries; incidents affecting shared service centers |
The threshold is capability, not confirmed impact. If the incident could cause severe disruption or affect other persons, it triggers reporting — you do not need to wait for confirmed damage.
Who to Notify
| Recipient | When | How |
|---|---|---|
| National CSIRT | All three stages (24h, 72h, 1 month) | Per your Member State's designated CSIRT contact mechanism (typically web portal, email, or API) |
| Competent authority | If different from CSIRT — varies by Member State | Same content as CSIRT notification |
| Recipients of services | Without undue delay, if needed to mitigate the threat | When the significant incident may affect recipients' ability to provision their own services |
| Cross-border CSIRTs | When cross-border impact exists | Coordinated through your national CSIRT per Art.23§5 |
| ENISA | Aggregated by CSIRT — not your direct obligation | No direct action needed, but be prepared for ENISA follow-up questions |
Cross-Border Notification Requirements (Art.23§5)
If a significant incident affects services or entities in other EU Member States, the national CSIRT receiving your notification is obligated to inform other affected Member States' CSIRTs. However, you must:
- Flag the cross-border nature in your early warning
- Identify which Member States may be affected
- Describe the nature of the cross-border impact
- Be prepared to cooperate with multiple CSIRTs simultaneously
For ServiceNow: if your instance serves users or processes data in multiple EU countries, nearly every significant incident has potential cross-border impact. Default to yes in the cross-border field unless you are certain the impact is confined to a single Member State.
Incident Classification Criteria
The following table maps ServiceNow-specific incident characteristics to NIS2 Art.23§3 significance triggers. Use this to determine whether a security incident requires NIS2 reporting.
Classification Decision Table
| # | Trigger Condition | ServiceNow Fields/Context | NIS2 Criterion Met | Auto-Classify? |
|---|---|---|---|---|
| 1 | Data breach affecting personal data of > 500 individuals | sn_si_incident.data_elements contains PII categories; estimated records > 500 |
Affected other persons (material damage) | Yes |
| 2 | Service disruption > 4 hours on critical business processes | incident.priority = 1 or 2; incident.business_duration > 4h; cmdb_ci_service marked as critical |
Severe operational disruption | Yes |
| 3 | Active exploitation of known CVE (CISA KEV listed) | sn_si_incident linked to sn_vul_entry with CVE in KEV catalog |
Severe operational disruption + potentially affected other persons | Yes |
| 4 | Ransomware or destructive malware confirmed | sn_si_incident.attack_type = ransomware/wiper; encryption or destruction of data confirmed |
Severe operational disruption + financial loss | Yes |
| 5 | Unauthorized administrative access confirmed | Backdoor admin accounts detected (see Incident Response Playbook for Critical CVE Exploitation); unauthorized role grants to admin or security_admin |
Severe operational disruption + affected other persons | Yes |
| 6 | Credential theft affecting downstream systems | Discovery credentials exfiltrated (discovery_credentials); MID Server compromised; OAuth tokens stolen |
Affected other persons; cross-border if multi-national | Yes |
| 7 | Supply chain compromise via ServiceNow integrations | Integration Hub or MID Server used as pivot; third-party system accessed via stolen ServiceNow credentials | Affected other persons; cross-border | Manual |
| 8 | DDoS causing > 4 hours of service unavailability | Instance unreachable; ServiceNow HI confirms platform-level attack | Severe operational disruption | Manual |
| 9 | Insider threat with data exfiltration | Bulk data export detected (see Incident Response Playbook for Critical CVE Exploitation IOC Script 5); unusual download patterns on sensitive tables | Affected other persons | Manual |
| 10 | Multi-tenant impact (shared ServiceNow instance) | Multiple business units or legal entities affected on same instance | Cross-border if entities span Member States | Manual |
Mapping to ServiceNow Incident Priority/Severity
| ServiceNow Priority | ServiceNow Severity | NIS2 Classification Logic |
|---|---|---|
| P1 — Critical | 1 — High | Likely reportable. Evaluate against classification table above. If any trigger matches, classify as NIS2-reportable. |
| P2 — High | 1 — High | Possibly reportable. Check duration (> 4h?) and data impact. If security incident, evaluate triggers 1-6. |
| P3 — Moderate | 2 — Medium | Unlikely reportable unless data breach scope expands or duration exceeds threshold. |
| P4 — Low | 3 — Low | Not reportable under normal circumstances. Re-evaluate if scope changes. |
For Security Incidents (sn_si_incident), always evaluate against the classification table regardless of priority — a "low priority" security incident can still be NIS2-reportable if it involves data breach or credential theft.
24-Hour Early Warning Implementation
What Must Be Included (Per Art.23§4(a))
The early warning is deliberately lightweight. It is designed to be submittable fast, even before you fully understand the incident. The regulation requires:
- Whether the significant incident is suspected to be caused by unlawful or malicious acts — a yes/no assessment. You do not need proof; suspicion is sufficient.
- Whether the incident could have cross-border impact — again, a yes/no assessment based on your initial understanding.
- Any other relevant initial information — brief description of what was detected.
That is all. The early warning is not a root cause analysis. It is a signal to the CSIRT that something significant has happened and they should be prepared.
ServiceNow Custom Table: u_nis2_incident_report
Create this table to track NIS2-reportable incidents and their reporting milestones. This is the central record that links to the security incident and holds all three reporting stages.
Table Name: u_nis2_incident_report
Label: NIS2 Incident Report
Extends: Task (task)
Fields:
┌────────────────────────────┬──────────────┬──────────────────────────────────────┐
│ Field Name │ Type │ Purpose │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_security_incident │ Reference │ Link to sn_si_incident │
│ │ (sn_si_inc.) │ │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_nis2_classification │ Choice │ significant / not_significant / │
│ │ │ under_review │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_trigger_criteria │ String (256) │ Which classification trigger(s) met │
│ │ │ (from decision table above) │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_awareness_datetime │ Date/Time │ When the entity became "aware" │
│ │ │ (this starts ALL clocks) │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_suspected_malicious │ True/False │ Art.23§4(a): unlawful/malicious? │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_cross_border_impact │ True/False │ Art.23§4(a): cross-border? │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_member_states_affected │ String (512) │ List of EU member states affected │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_early_warning_status │ Choice │ pending / submitted / overdue │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_early_warning_submitted │ Date/Time │ Actual submission timestamp │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_early_warning_content │ Journal │ Content submitted in early warning │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_notification_status │ Choice │ pending / submitted / overdue │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_notification_submitted │ Date/Time │ Actual submission timestamp │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_notification_content │ Journal │ Content submitted at 72h │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_severity_assessment │ Choice │ critical / high / medium / low │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_impact_description │ String (4000)│ Description of impact on services │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_ioc_summary │ String (4000)│ Indicators of compromise summary │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_affected_services │ List │ cmdb_ci_service references │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_estimated_affected_users │ Integer │ Estimated number of affected users │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_final_report_status │ Choice │ pending / submitted / overdue / │
│ │ │ progress_report_submitted │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_final_report_submitted │ Date/Time │ Actual final report submission │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_final_report_content │ Journal │ Content of final report │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_root_cause │ String (4000)│ Root cause analysis │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_remediation_actions │ String (4000)│ Mitigation measures applied │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_lessons_learned │ String (4000)│ Lessons learned / control │
│ │ │ improvements per Art.21§2(f) │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_csirt_reference │ String (128) │ CSIRT's reference number for │
│ │ │ the notification │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_csirt_contact_method │ Choice │ portal / email / api / phone │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_gdpr_notification_req │ True/False │ Does this also trigger GDPR Art.33? │
├────────────────────────────┼──────────────┼──────────────────────────────────────┤
│ u_dora_notification_req │ True/False │ Does this also trigger DORA Art.17? │
└────────────────────────────┴──────────────┴──────────────────────────────────────┘
Business Rule: NIS2 Classification Check on Security Incident Creation
/*
* NIS2-23 Business Rule: NIS2 Classification Check
* Triggers when a security incident is created or updated with high severity
* Evaluates whether the incident meets Art.23§3 significance criteria
*
* Table: sn_si_incident (Security Incident)
* When: After Insert, After Update
* Condition: current.priority <= 2 || current.severity <= 2 ||
* current.substate == 'confirmed'
* Order: 200
*
* Run as: Background script with admin or sn_si.admin role
* Impact: Creates u_nis2_incident_report record if criteria met
* Versions: Washington+
*/
(function executeRule(current, previous) {
// Avoid duplicate classification on update if already classified
if (current.operation() === 'update') {
var existingReport = new GlideRecord('u_nis2_incident_report');
existingReport.addQuery('u_security_incident', current.getUniqueValue());
existingReport.query();
if (existingReport.hasNext()) {
// Already classified — do not re-evaluate
// Update logic is handled by the SLA and escalation workflows
return;
}
}
var triggers = [];
var isSignificant = false;
// Trigger 1: Data breach affecting > 500 individuals
var estimatedRecords = parseInt(current.getValue('u_estimated_affected_records') || '0');
var dataElements = current.getValue('data_elements') || '';
if (estimatedRecords > 500 && dataElements.length > 0) {
triggers.push('T1: Data breach > 500 individuals');
isSignificant = true;
}
// Trigger 2: Critical/high priority with extended duration
var priority = parseInt(current.getValue('priority') || '4');
if (priority <= 2) {
// Check if associated ITSM incident has > 4h business duration
var relatedIncident = current.getValue('u_related_incident');
if (relatedIncident) {
var itsm = new GlideRecord('incident');
if (itsm.get(relatedIncident)) {
var durSec = parseInt(itsm.getValue('business_duration') || '0');
if (durSec > 14400) { // 4 hours in seconds
triggers.push('T2: Service disruption > 4h on P' + priority);
isSignificant = true;
}
}
}
// Also flag if priority is 1 regardless of duration
if (priority === 1) {
triggers.push('T2: P1 critical incident — potential severe disruption');
isSignificant = true;
}
}
// Trigger 3: CVE exploitation confirmed
var attackType = current.getValue('attack_type') || '';
var category = current.getValue('category') || '';
if (category.indexOf('vulnerability') > -1 || attackType.indexOf('exploit') > -1) {
triggers.push('T3: CVE/vulnerability exploitation');
isSignificant = true;
}
// Trigger 4: Ransomware or destructive malware
if (attackType.indexOf('ransomware') > -1 || attackType.indexOf('wiper') > -1 ||
attackType.indexOf('destructive') > -1) {
triggers.push('T4: Ransomware/destructive malware');
isSignificant = true;
}
// Trigger 5: Unauthorized admin access
if (attackType.indexOf('privilege') > -1 || attackType.indexOf('escalation') > -1 ||
category.indexOf('unauthorized') > -1) {
triggers.push('T5: Unauthorized administrative access');
isSignificant = true;
}
// Trigger 6: Credential theft
if (category.indexOf('credential') > -1 || attackType.indexOf('credential') > -1 ||
dataElements.indexOf('credential') > -1 || dataElements.indexOf('password') > -1) {
triggers.push('T6: Credential theft affecting downstream systems');
isSignificant = true;
}
// If significant, create the NIS2 report record
if (isSignificant) {
var report = new GlideRecord('u_nis2_incident_report');
report.initialize();
report.setValue('u_security_incident', current.getUniqueValue());
report.setValue('u_nis2_classification', 'significant');
report.setValue('u_trigger_criteria', triggers.join('; '));
report.setValue('u_awareness_datetime', new GlideDateTime().getValue());
report.setValue('u_early_warning_status', 'pending');
report.setValue('u_notification_status', 'pending');
report.setValue('u_final_report_status', 'pending');
report.setValue('short_description',
'NIS2 Art.23 Report: ' + current.getValue('short_description'));
report.setValue('assignment_group',
current.getValue('assignment_group'));
// Default cross-border to true if instance serves multiple countries
// Adjust this logic to your organization
report.setValue('u_cross_border_impact', true);
// Check if GDPR notification is also triggered
if (dataElements.indexOf('personal') > -1 || dataElements.indexOf('PII') > -1 ||
estimatedRecords > 0) {
report.setValue('u_gdpr_notification_req', true);
}
// Check if DORA notification is also triggered (financial entities)
// Set this based on your organization's DORA scope
var isDORAEntity = gs.getProperty('u_nis2.dora_in_scope', 'false') === 'true';
if (isDORAEntity) {
report.setValue('u_dora_notification_req', true);
}
var reportId = report.insert();
gs.info('[NIS2-23] Significant incident classified. ' +
'NIS2 report created: ' + reportId +
' | Triggers: ' + triggers.join('; ') +
' | Security Incident: ' + current.getValue('number'));
// Trigger notification to NIS2 response team
gs.eventQueue('u_nis2.significant_incident_detected', report,
current.getValue('number'), triggers.join('; '));
}
})(current, previous);
SLA Definitions for 24h / 72h / 1-Month Deadlines
Create three SLA definitions attached to the u_nis2_incident_report table. These enforce the regulatory deadlines with automated escalation.
/*
* NIS2-23 SLA Definition Script
* Run this as a background script to create the three NIS2 SLA definitions
*
* Run as: Background script with admin role
* Impact: Creates SLA definition records
* Versions: Washington+
*
* IMPORTANT: Run this ONCE during initial setup. The script checks for
* existing records to avoid duplicates.
*/
gs.info('=== NIS2-23: CREATING SLA DEFINITIONS ===');
// Helper function to create or update an SLA definition
function createNIS2SLA(name, duration, statusField) {
var existing = new GlideRecord('contract_sla');
existing.addQuery('name', name);
existing.query();
if (existing.hasNext()) {
gs.info('SLA "' + name + '" already exists — skipping');
return;
}
var sla = new GlideRecord('contract_sla');
sla.initialize();
sla.setValue('name', name);
sla.setValue('collection', 'u_nis2_incident_report');
sla.setValue('active', true);
sla.setValue('type', 'SLA');
// Duration settings
sla.setValue('duration_type', 'user_specified');
sla.setValue('duration', duration);
// Start condition: NIS2 classification = significant AND stage status = pending
sla.setValue('start_condition',
'u_nis2_classification=significant^' + statusField + '=pending');
// Stop condition: stage status = submitted
sla.setValue('stop_condition', statusField + '=submitted');
// Pause condition: none — regulatory deadlines do not pause
sla.setValue('pause_condition', '');
// Reset condition: classification changed to not_significant
sla.setValue('reset_condition', 'u_nis2_classification=not_significant');
sla.insert();
gs.info('SLA "' + name + '" created successfully');
}
// SLA 1: 24-hour early warning
createNIS2SLA(
'NIS2 Art.23 — 24h Early Warning',
'1970-01-02 00:00:00', // 24 hours (GlideDuration format)
'u_early_warning_status'
);
// SLA 2: 72-hour incident notification
createNIS2SLA(
'NIS2 Art.23 — 72h Incident Notification',
'1970-01-04 00:00:00', // 72 hours
'u_notification_status'
);
// SLA 3: 1-month final report
createNIS2SLA(
'NIS2 Art.23 — 30d Final Report',
'1970-01-31 00:00:00', // 30 days
'u_final_report_status'
);
gs.info('');
gs.info('=== SLA DEFINITIONS CREATED ===');
gs.info('Next steps:');
gs.info('1. Verify SLA definitions in: contract_sla_list.do');
gs.info('2. Configure SLA notification rules (see escalation section below)');
gs.info('3. Test with a sample NIS2 incident report record');
Early Warning Notification Template
/*
* NIS2-23 Notification Template: 24-Hour Early Warning
*
* This script generates the early warning content from the NIS2 report record.
* Use in an email notification or as the basis for CSIRT portal submission.
*
* Can be called from:
* - Email notification template (mail script)
* - Scripted REST API endpoint (for automated CSIRT API submission)
* - UI action on the u_nis2_incident_report form
*
* Versions: Washington+
*/
var NIS2EarlyWarningTemplate = Class.create();
NIS2EarlyWarningTemplate.prototype = {
initialize: function(reportSysId) {
this.report = new GlideRecord('u_nis2_incident_report');
if (!this.report.get(reportSysId)) {
throw new Error('NIS2 report not found: ' + reportSysId);
}
this.secIncident = new GlideRecord('sn_si_incident');
this.secIncident.get(this.report.getValue('u_security_incident'));
},
generate: function() {
var gdt = new GlideDateTime();
var orgName = gs.getProperty('glide.servlet.uri', 'ORGANIZATION');
var content = [];
content.push('═══════════════════════════════════════════════════════');
content.push('NIS2 ARTICLE 23§4(a) — EARLY WARNING NOTIFICATION');
content.push('═══════════════════════════════════════════════════════');
content.push('');
content.push('REPORTING ENTITY');
content.push(' Organization: ' + gs.getProperty('u_nis2.org_name', orgName));
content.push(' NIS2 Entity Type: ' +
gs.getProperty('u_nis2.entity_type', 'Essential / Important'));
content.push(' Sector: ' +
gs.getProperty('u_nis2.sector', '[Configure u_nis2.sector]'));
content.push(' Contact Person: ' +
gs.getProperty('u_nis2.contact_name', '[Configure u_nis2.contact_name]'));
content.push(' Contact Email: ' +
gs.getProperty('u_nis2.contact_email', '[Configure u_nis2.contact_email]'));
content.push(' Contact Phone: ' +
gs.getProperty('u_nis2.contact_phone', '[Configure u_nis2.contact_phone]'));
content.push('');
content.push('INCIDENT IDENTIFICATION');
content.push(' Internal Reference: ' + this.report.getValue('number'));
content.push(' Security Incident: ' + this.secIncident.getValue('number'));
content.push(' Awareness Date/Time: ' +
this.report.getDisplayValue('u_awareness_datetime'));
content.push(' Report Generated: ' + gdt.getDisplayValue());
content.push('');
content.push('EARLY WARNING ASSESSMENT');
content.push('');
content.push(' 1. Is the incident suspected to be caused by unlawful');
content.push(' or malicious acts?');
content.push(' ANSWER: ' +
(this.report.getValue('u_suspected_malicious') === '1' ? 'YES' : 'NO'));
content.push('');
content.push(' 2. Could the incident have cross-border impact?');
content.push(' ANSWER: ' +
(this.report.getValue('u_cross_border_impact') === '1' ? 'YES' : 'NO'));
var memberStates = this.report.getValue('u_member_states_affected');
if (memberStates) {
content.push(' Potentially affected Member States: ' + memberStates);
}
content.push('');
content.push(' 3. Initial description of the incident:');
content.push(' ' + (this.secIncident.getValue('short_description') || 'N/A'));
content.push('');
content.push(' 4. Classification trigger(s):');
content.push(' ' + (this.report.getValue('u_trigger_criteria') || 'N/A'));
content.push('');
content.push('ADDITIONAL INFORMATION');
content.push(' Incident Priority: ' +
this.secIncident.getDisplayValue('priority'));
content.push(' Incident Category: ' +
(this.secIncident.getDisplayValue('category') || 'N/A'));
content.push(' Attack Type: ' +
(this.secIncident.getDisplayValue('attack_type') || 'Under investigation'));
content.push('');
content.push('NOTE: This is an early warning per NIS2 Art.23§4(a).');
content.push('A full incident notification will follow within 72 hours');
content.push('of the awareness date/time specified above.');
content.push('');
content.push('═══════════════════════════════════════════════════════');
return content.join('\n');
},
// Mark early warning as submitted and log the action
markSubmitted: function(csirtReference) {
this.report.setValue('u_early_warning_status', 'submitted');
this.report.setValue('u_early_warning_submitted', new GlideDateTime().getValue());
if (csirtReference) {
this.report.setValue('u_csirt_reference', csirtReference);
}
this.report.update();
// Log to the journal for audit trail
this.report.work_notes = 'Early warning (Art.23§4(a)) submitted to CSIRT. ' +
(csirtReference ? 'CSIRT Reference: ' + csirtReference : '') +
' | Submitted at: ' + new GlideDateTime().getDisplayValue();
this.report.update();
gs.info('[NIS2-23] Early warning submitted for ' + this.report.getValue('number'));
},
type: 'NIS2EarlyWarningTemplate'
};
Escalation When SLA at Risk
/*
* NIS2-23 Business Rule: SLA Breach Escalation
* Monitors NIS2 report SLAs and escalates when approaching breach
*
* Table: task_sla
* When: After Update
* Condition: current.sla.collection = 'u_nis2_incident_report' &&
* current.sla.name.startsWith('NIS2') &&
* current.stage != previous.stage
* Order: 100
*
* Versions: Washington+
*/
(function executeRule(current, previous) {
var slaName = current.getDisplayValue('sla');
var stage = current.getValue('stage');
var taskSysId = current.getValue('task');
// Only process NIS2 SLAs
if (slaName.indexOf('NIS2') === -1) return;
var report = new GlideRecord('u_nis2_incident_report');
if (!report.get(taskSysId)) return;
var reportNumber = report.getValue('number');
// Determine which deadline this SLA tracks
var deadlineType = '';
var statusField = '';
if (slaName.indexOf('24h') > -1) {
deadlineType = '24-hour early warning';
statusField = 'u_early_warning_status';
} else if (slaName.indexOf('72h') > -1) {
deadlineType = '72-hour incident notification';
statusField = 'u_notification_status';
} else if (slaName.indexOf('30d') > -1) {
deadlineType = '30-day final report';
statusField = 'u_final_report_status';
}
// Stage-based escalation logic
// SLA stages: has_breached, in_progress, paused
// Percentage thresholds trigger stage changes at 50%, 75%, 100%
var percentage = parseFloat(current.getValue('percentage') || '0');
if (stage === 'in_progress') {
if (percentage >= 75 && percentage < 100) {
// 75% — URGENT: deadline approaching
gs.eventQueue('u_nis2.sla_75_percent', report,
deadlineType, reportNumber);
report.work_notes = 'WARNING: NIS2 ' + deadlineType +
' deadline is at 75% (' + Math.round(percentage) +
'%). Immediate action required to avoid regulatory breach.';
report.update();
gs.info('[NIS2-23] SLA 75% warning: ' + deadlineType +
' for ' + reportNumber);
} else if (percentage >= 50 && percentage < 75) {
// 50% — WARNING: half the time has elapsed
gs.eventQueue('u_nis2.sla_50_percent', report,
deadlineType, reportNumber);
report.work_notes = 'NOTICE: NIS2 ' + deadlineType +
' deadline is at 50%. Ensure report content is being prepared.';
report.update();
gs.info('[NIS2-23] SLA 50% notice: ' + deadlineType +
' for ' + reportNumber);
}
}
if (stage === 'has_breached') {
// 100% — BREACHED: regulatory deadline missed
report.setValue(statusField, 'overdue');
report.update();
gs.eventQueue('u_nis2.sla_breached', report,
deadlineType, reportNumber);
report.work_notes = 'CRITICAL: NIS2 ' + deadlineType +
' deadline has been BREACHED. This constitutes a regulatory ' +
'non-compliance event under NIS2 Art.23. Supervisory action ' +
'and fines up to EUR 10M or 2% of global turnover may apply. ' +
'Submit the notification IMMEDIATELY and document the delay reason.';
report.update();
// Escalate to executive level
gs.eventQueue('u_nis2.regulatory_breach', report,
'NIS2 Art.23 ' + deadlineType + ' BREACHED', reportNumber);
gs.warn('[NIS2-23] SLA BREACHED: ' + deadlineType +
' for ' + reportNumber + ' — REGULATORY NON-COMPLIANCE');
}
})(current, previous);
72-Hour Full Notification Implementation
What Must Be Included (Per Art.23§4(b))
The 72-hour notification is a substantial update to the early warning. It must include:
- Update to the early warning — any changes to the malicious/cross-border assessment
- Initial assessment of the incident — severity, impact scope, and nature
- Indicators of compromise — where available at this stage
- Affected services and user impact — what is disrupted, how many people affected
72-Hour Notification Template
/*
* NIS2-23 Notification Template: 72-Hour Incident Notification
*
* Generates the full incident notification content.
* Extends the early warning with impact assessment and IOCs.
*
* Versions: Washington+
*/
var NIS2FullNotificationTemplate = Class.create();
NIS2FullNotificationTemplate.prototype = {
initialize: function(reportSysId) {
this.report = new GlideRecord('u_nis2_incident_report');
if (!this.report.get(reportSysId)) {
throw new Error('NIS2 report not found: ' + reportSysId);
}
this.secIncident = new GlideRecord('sn_si_incident');
this.secIncident.get(this.report.getValue('u_security_incident'));
},
generate: function() {
var gdt = new GlideDateTime();
var content = [];
content.push('═══════════════════════════════════════════════════════');
content.push('NIS2 ARTICLE 23§4(b) — INCIDENT NOTIFICATION');
content.push('═══════════════════════════════════════════════════════');
content.push('');
// Section 1: Reporting entity (same as early warning)
content.push('REPORTING ENTITY');
content.push(' Organization: ' +
gs.getProperty('u_nis2.org_name', '[Configure]'));
content.push(' NIS2 Entity Type: ' +
gs.getProperty('u_nis2.entity_type', 'Essential / Important'));
content.push(' Sector: ' +
gs.getProperty('u_nis2.sector', '[Configure]'));
content.push(' Contact Person: ' +
gs.getProperty('u_nis2.contact_name', '[Configure]'));
content.push(' Contact Email: ' +
gs.getProperty('u_nis2.contact_email', '[Configure]'));
content.push(' Contact Phone: ' +
gs.getProperty('u_nis2.contact_phone', '[Configure]'));
content.push('');
// Section 2: Incident identification
content.push('INCIDENT IDENTIFICATION');
content.push(' Internal Reference: ' + this.report.getValue('number'));
content.push(' CSIRT Reference: ' +
(this.report.getValue('u_csirt_reference') || 'Pending'));
content.push(' Security Incident: ' + this.secIncident.getValue('number'));
content.push(' Awareness Date/Time: ' +
this.report.getDisplayValue('u_awareness_datetime'));
content.push(' Early Warning Sent: ' +
this.report.getDisplayValue('u_early_warning_submitted'));
content.push(' This Report: ' + gdt.getDisplayValue());
content.push('');
// Section 3: Update to early warning
content.push('UPDATE TO EARLY WARNING');
content.push(' Suspected malicious: ' +
(this.report.getValue('u_suspected_malicious') === '1' ? 'YES' : 'NO'));
content.push(' Cross-border impact: ' +
(this.report.getValue('u_cross_border_impact') === '1' ? 'YES' : 'NO'));
var memberStates = this.report.getValue('u_member_states_affected');
if (memberStates) {
content.push(' Member States: ' + memberStates);
}
content.push('');
// Section 4: Severity and impact assessment
content.push('SEVERITY AND IMPACT ASSESSMENT');
content.push(' Severity: ' +
(this.report.getDisplayValue('u_severity_assessment') || 'Under assessment'));
content.push(' Incident Priority: ' +
this.secIncident.getDisplayValue('priority'));
content.push(' Attack Type: ' +
(this.secIncident.getDisplayValue('attack_type') || 'Under investigation'));
content.push(' Category: ' +
(this.secIncident.getDisplayValue('category') || 'N/A'));
content.push('');
content.push(' Estimated Affected Users: ' +
(this.report.getValue('u_estimated_affected_users') || 'Under assessment'));
content.push('');
// Impact description
content.push(' Impact Description:');
var impactDesc = this.report.getValue('u_impact_description') ||
'Impact assessment is ongoing.';
var impactLines = impactDesc.split('\n');
for (var il = 0; il < impactLines.length; il++) {
content.push(' ' + impactLines[il]);
}
content.push('');
// Section 5: Affected services
content.push('AFFECTED SERVICES');
var services = this.report.getValue('u_affected_services');
if (services) {
var svcIds = services.split(',');
for (var s = 0; s < svcIds.length; s++) {
var svc = new GlideRecord('cmdb_ci_service');
if (svc.get(svcIds[s])) {
content.push(' - ' + svc.getValue('name') +
' (Class: ' + svc.getValue('sys_class_name') +
', Status: ' + svc.getDisplayValue('operational_status') + ')');
}
}
} else {
content.push(' Service impact assessment in progress.');
}
content.push('');
// Section 6: Indicators of compromise
content.push('INDICATORS OF COMPROMISE');
var iocSummary = this.report.getValue('u_ioc_summary');
if (iocSummary) {
var iocLines = iocSummary.split('\n');
for (var io = 0; io < iocLines.length; io++) {
content.push(' ' + iocLines[io]);
}
} else {
content.push(' IOC collection is ongoing. Details will be provided');
content.push(' in the final report per Art.23§4(d).');
}
content.push('');
// Section 7: Response measures
content.push('RESPONSE MEASURES TAKEN');
content.push(' Status: ' +
this.secIncident.getDisplayValue('state'));
// Pull response actions from the security incident work notes
content.push(' Containment actions: Documented in work notes of ' +
this.secIncident.getValue('number'));
content.push('');
// Section 8: Concurrent regulatory notifications
content.push('CONCURRENT REGULATORY OBLIGATIONS');
content.push(' GDPR Art.33 notification required: ' +
(this.report.getValue('u_gdpr_notification_req') === '1' ? 'YES' : 'NO'));
content.push(' DORA Art.17 notification required: ' +
(this.report.getValue('u_dora_notification_req') === '1' ? 'YES' : 'NO'));
content.push('');
content.push('NOTE: A final report per NIS2 Art.23§4(d) will be submitted');
content.push('within 1 month of the initial incident notification.');
content.push('');
content.push('═══════════════════════════════════════════════════════');
return content.join('\n');
},
markSubmitted: function(csirtReference) {
this.report.setValue('u_notification_status', 'submitted');
this.report.setValue('u_notification_submitted', new GlideDateTime().getValue());
if (csirtReference) {
this.report.setValue('u_csirt_reference', csirtReference);
}
this.report.update();
this.report.work_notes = '72-hour incident notification (Art.23§4(b)) ' +
'submitted to CSIRT. ' +
(csirtReference ? 'CSIRT Reference: ' + csirtReference : '') +
' | Submitted at: ' + new GlideDateTime().getDisplayValue();
this.report.update();
gs.info('[NIS2-23] 72h notification submitted for ' +
this.report.getValue('number'));
},
type: 'NIS2FullNotificationTemplate'
};
1-Month Final Report Implementation
What Must Be Included (Per Art.23§4(d))
The final report is the most comprehensive document. It is due one month after the incident notification (72-hour stage), or one month after the incident is resolved if it is still ongoing at the 30-day mark. It must include:
- Detailed description of the incident — including severity, scope, and timeline
- Type of threat or root cause — that likely triggered the incident
- Applied and ongoing mitigation measures — what was done and what is still being done
- Cross-border impact — if applicable, the extent of impact on other Member States
Root Cause Analysis Fields
The final report ties directly back to NIS2 Art.21§2(f), which requires entities to have "policies and procedures to assess the effectiveness of cybersecurity risk-management measures." The root cause analysis must feed into control improvements.
| Final Report Section | Source in ServiceNow | NIS2 Reference |
|---|---|---|
| Incident timeline | Security incident work notes, sys_audit trail | Art.23§4(d)(i) |
| Root cause analysis | u_nis2_incident_report.u_root_cause |
Art.23§4(d)(ii) |
| Threat type classification | sn_si_incident.attack_type + analysis |
Art.23§4(d)(ii) |
| Remediation actions taken | u_nis2_incident_report.u_remediation_actions |
Art.23§4(d)(iii) |
| Cross-border impact assessment | u_nis2_incident_report.u_cross_border_impact + details |
Art.23§4(d)(iv) |
| Lessons learned | u_nis2_incident_report.u_lessons_learned |
Art.21§2(f) |
| Control improvements planned | Linked change requests / problem records | Art.21§2(f) |
Final Report Template
/*
* NIS2-23 Notification Template: 1-Month Final Report
*
* Generates the comprehensive final report content.
* Includes root cause analysis, remediation actions, and lessons learned.
*
* Versions: Washington+
*/
var NIS2FinalReportTemplate = Class.create();
NIS2FinalReportTemplate.prototype = {
initialize: function(reportSysId) {
this.report = new GlideRecord('u_nis2_incident_report');
if (!this.report.get(reportSysId)) {
throw new Error('NIS2 report not found: ' + reportSysId);
}
this.secIncident = new GlideRecord('sn_si_incident');
this.secIncident.get(this.report.getValue('u_security_incident'));
},
generate: function() {
var gdt = new GlideDateTime();
var content = [];
content.push('═══════════════════════════════════════════════════════');
content.push('NIS2 ARTICLE 23§4(d) — FINAL INCIDENT REPORT');
content.push('═══════════════════════════════════════════════════════');
content.push('');
// Section 1: Reporting entity
content.push('REPORTING ENTITY');
content.push(' Organization: ' +
gs.getProperty('u_nis2.org_name', '[Configure]'));
content.push(' NIS2 Entity Type: ' +
gs.getProperty('u_nis2.entity_type', '[Configure]'));
content.push(' Sector: ' +
gs.getProperty('u_nis2.sector', '[Configure]'));
content.push(' Contact Person: ' +
gs.getProperty('u_nis2.contact_name', '[Configure]'));
content.push(' Contact Email: ' +
gs.getProperty('u_nis2.contact_email', '[Configure]'));
content.push('');
// Section 2: Report history
content.push('NOTIFICATION HISTORY');
content.push(' Internal Reference: ' + this.report.getValue('number'));
content.push(' CSIRT Reference: ' +
(this.report.getValue('u_csirt_reference') || 'N/A'));
content.push(' Awareness Date/Time: ' +
this.report.getDisplayValue('u_awareness_datetime'));
content.push(' Early Warning Sent: ' +
this.report.getDisplayValue('u_early_warning_submitted'));
content.push(' 72h Notification: ' +
this.report.getDisplayValue('u_notification_submitted'));
content.push(' Final Report Date: ' + gdt.getDisplayValue());
content.push('');
// Section 3: Detailed incident description
content.push('═══════════════════════════════════════════════════════');
content.push('SECTION 1: DETAILED INCIDENT DESCRIPTION');
content.push('═══════════════════════════════════════════════════════');
content.push('');
content.push(' Incident Summary: ' +
this.secIncident.getValue('short_description'));
content.push(' Severity: ' +
this.report.getDisplayValue('u_severity_assessment'));
content.push(' Priority: ' +
this.secIncident.getDisplayValue('priority'));
content.push(' Attack Type: ' +
(this.secIncident.getDisplayValue('attack_type') || 'N/A'));
content.push(' Affected Users: ' +
(this.report.getValue('u_estimated_affected_users') || 'N/A'));
content.push('');
// Impact description
content.push(' Impact Assessment:');
var impact = this.report.getValue('u_impact_description') || 'N/A';
var impactLines = impact.split('\n');
for (var i = 0; i < impactLines.length; i++) {
content.push(' ' + impactLines[i]);
}
content.push('');
// Affected services
content.push(' Affected Services:');
var services = this.report.getValue('u_affected_services');
if (services) {
var svcIds = services.split(',');
for (var s = 0; s < svcIds.length; s++) {
var svc = new GlideRecord('cmdb_ci_service');
if (svc.get(svcIds[s])) {
content.push(' - ' + svc.getValue('name'));
}
}
} else {
content.push(' N/A');
}
content.push('');
// Incident timeline from audit trail
content.push(' Incident Timeline:');
content.push(' (Reconstructed from audit records and work notes)');
this._appendTimeline(content);
content.push('');
// Section 4: Threat type and root cause
content.push('═══════════════════════════════════════════════════════');
content.push('SECTION 2: THREAT TYPE AND ROOT CAUSE');
content.push('═══════════════════════════════════════════════════════');
content.push('');
var rootCause = this.report.getValue('u_root_cause') ||
'[ROOT CAUSE ANALYSIS NOT YET COMPLETED]';
var rcLines = rootCause.split('\n');
for (var rc = 0; rc < rcLines.length; rc++) {
content.push(' ' + rcLines[rc]);
}
content.push('');
// Section 5: Mitigation measures
content.push('═══════════════════════════════════════════════════════');
content.push('SECTION 3: MITIGATION MEASURES');
content.push('═══════════════════════════════════════════════════════');
content.push('');
var remediation = this.report.getValue('u_remediation_actions') ||
'[REMEDIATION ACTIONS NOT YET DOCUMENTED]';
var remLines = remediation.split('\n');
for (var rm = 0; rm < remLines.length; rm++) {
content.push(' ' + remLines[rm]);
}
content.push('');
// Section 6: Cross-border impact
content.push('═══════════════════════════════════════════════════════');
content.push('SECTION 4: CROSS-BORDER IMPACT');
content.push('═══════════════════════════════════════════════════════');
content.push('');
content.push(' Cross-border impact: ' +
(this.report.getValue('u_cross_border_impact') === '1' ? 'YES' : 'NO'));
var memberStates = this.report.getValue('u_member_states_affected');
if (memberStates) {
content.push(' Member States: ' + memberStates);
}
content.push('');
// Section 7: Lessons learned — feeds back to Art.21§2(f)
content.push('═══════════════════════════════════════════════════════');
content.push('SECTION 5: LESSONS LEARNED [Art.21§2(f)]');
content.push('═══════════════════════════════════════════════════════');
content.push('');
var lessons = this.report.getValue('u_lessons_learned') ||
'[LESSONS LEARNED NOT YET DOCUMENTED]';
var llLines = lessons.split('\n');
for (var ll = 0; ll < llLines.length; ll++) {
content.push(' ' + llLines[ll]);
}
content.push('');
// Section 8: Concurrent notifications
content.push('CONCURRENT REGULATORY NOTIFICATIONS');
content.push(' GDPR Art.33 notification: ' +
(this.report.getValue('u_gdpr_notification_req') === '1' ?
'YES — submitted' : 'Not applicable'));
content.push(' DORA Art.17 notification: ' +
(this.report.getValue('u_dora_notification_req') === '1' ?
'YES — submitted' : 'Not applicable'));
content.push('');
content.push('═══════════════════════════════════════════════════════');
content.push('END OF FINAL REPORT');
content.push('═══════════════════════════════════════════════════════');
return content.join('\n');
},
_appendTimeline: function(content) {
// Reconstruct timeline from security incident audit trail
var audit = new GlideRecord('sys_audit');
audit.addQuery('documentkey', this.secIncident.getUniqueValue());
audit.addQuery('tablename', 'sn_si_incident');
audit.addQuery('fieldname', 'IN', 'state,priority,severity,assignment_group,' +
'assigned_to,substate,close_code');
audit.orderBy('sys_created_on');
audit.setLimit(50);
audit.query();
while (audit.next()) {
content.push(' ' + audit.getValue('sys_created_on') +
' | ' + audit.getValue('fieldname') +
': ' + (audit.getValue('oldvalue') || '(empty)') +
' → ' + (audit.getValue('newvalue') || '(empty)') +
' | By: ' + audit.getValue('user'));
}
if (!audit.hasNext() && audit.getRowCount() === 0) {
content.push(' No audit trail entries found for this incident.');
}
},
markSubmitted: function(csirtReference) {
this.report.setValue('u_final_report_status', 'submitted');
this.report.setValue('u_final_report_submitted', new GlideDateTime().getValue());
if (csirtReference) {
this.report.setValue('u_csirt_reference', csirtReference);
}
this.report.update();
this.report.work_notes = 'Final report (Art.23§4(d)) submitted to CSIRT. ' +
(csirtReference ? 'CSIRT Reference: ' + csirtReference : '') +
' | Submitted at: ' + new GlideDateTime().getDisplayValue();
this.report.update();
gs.info('[NIS2-23] Final report submitted for ' +
this.report.getValue('number'));
},
// For ongoing incidents at the 30-day mark
submitProgressReport: function() {
this.report.setValue('u_final_report_status', 'progress_report_submitted');
this.report.update();
this.report.work_notes = 'Progress report (Art.23§4(e)) submitted ' +
'in lieu of final report — incident is still ongoing. ' +
'Final report due within 1 month of incident resolution.';
this.report.update();
gs.info('[NIS2-23] Progress report submitted for ' +
this.report.getValue('number') + ' — incident ongoing');
},
type: 'NIS2FinalReportTemplate'
};
ServiceNow Implementation Scripts
Evidence Trail Script — Audit All Actions and Timestamps
This script ensures that every action taken during the NIS2 reporting lifecycle is logged with timestamps, user attribution, and content hashes for regulatory proof. Regulators need to verify not just what was submitted, but when each decision was made and who made it.
/*
* NIS2-23 Script Include: Evidence Trail Manager
*
* Provides tamper-evident logging for all NIS2 reporting actions.
* Stores evidence in a dedicated table with content hashes.
*
* Table: u_nis2_evidence_log (create this table with the fields below)
*
* Fields:
* u_nis2_report (Reference → u_nis2_incident_report)
* u_action_type (Choice: classification, early_warning_generated,
* early_warning_submitted, notification_generated,
* notification_submitted, final_report_generated,
* final_report_submitted, escalation, field_change,
* sla_warning, sla_breach)
* u_action_by (Reference → sys_user)
* u_action_datetime (Date/Time)
* u_content_hash (String 128 — SHA-256 of content at time of action)
* u_content_snapshot (String 4000 — copy of content or field values)
* u_metadata (String 1000 — additional context)
*
* Versions: Washington+
*/
var NIS2EvidenceTrail = Class.create();
NIS2EvidenceTrail.prototype = {
initialize: function() {
// No initialization needed — all methods are utility functions
},
/*
* Log an action with content hash for tamper detection.
*
* @param {string} reportSysId — sys_id of the u_nis2_incident_report record
* @param {string} actionType — one of the defined action types
* @param {string} contentSnapshot — the content being logged (will be hashed)
* @param {string} metadata — optional additional context
*/
logAction: function(reportSysId, actionType, contentSnapshot, metadata) {
var log = new GlideRecord('u_nis2_evidence_log');
log.initialize();
log.setValue('u_nis2_report', reportSysId);
log.setValue('u_action_type', actionType);
log.setValue('u_action_by', gs.getUserID());
log.setValue('u_action_datetime', new GlideDateTime().getValue());
// Generate SHA-256 hash of the content for tamper detection
if (contentSnapshot) {
// ⚠️ VERIFY: confirm class exists in your SN release and scope [GlideDigest]
var hash = new GlideDigest().getSHA256Hex(contentSnapshot);
log.setValue('u_content_hash', hash);
// Truncate snapshot to 4000 chars for storage
log.setValue('u_content_snapshot',
contentSnapshot.substring(0, 4000));
}
if (metadata) {
log.setValue('u_metadata', metadata.substring(0, 1000));
}
log.insert();
gs.info('[NIS2-23 EVIDENCE] Action: ' + actionType +
' | Report: ' + reportSysId +
' | By: ' + gs.getUserName() +
' | Hash: ' + (log.getValue('u_content_hash') || 'N/A'));
},
/*
* Log a field change on the NIS2 report record.
* Call this from a business rule on u_nis2_incident_report (After Update).
*/
logFieldChange: function(current, previous, fieldName) {
var oldVal = previous.getValue(fieldName) || '';
var newVal = current.getValue(fieldName) || '';
if (oldVal !== newVal) {
var snapshot = 'Field: ' + fieldName +
' | Old: ' + oldVal +
' | New: ' + newVal;
this.logAction(
current.getUniqueValue(),
'field_change',
snapshot,
'Changed by: ' + gs.getUserName()
);
}
},
/*
* Generate a complete evidence chain report for a given NIS2 report.
* Use this to demonstrate regulatory compliance to auditors.
*/
generateEvidenceChain: function(reportSysId) {
var output = [];
output.push('═══════════════════════════════════════════════════════');
output.push('NIS2 ART.23 — EVIDENCE CHAIN REPORT');
output.push('═══════════════════════════════════════════════════════');
output.push('Generated: ' + new GlideDateTime().getDisplayValue());
output.push('Report: ' + reportSysId);
output.push('');
var log = new GlideRecord('u_nis2_evidence_log');
log.addQuery('u_nis2_report', reportSysId);
log.orderBy('u_action_datetime');
log.query();
var count = 0;
while (log.next()) {
count++;
output.push('--- Event #' + count + ' ---');
output.push(' Date/Time: ' + log.getDisplayValue('u_action_datetime'));
output.push(' Action: ' + log.getValue('u_action_type'));
output.push(' Performed by: ' + log.getDisplayValue('u_action_by'));
output.push(' Content Hash: ' + (log.getValue('u_content_hash') || 'N/A'));
output.push(' Metadata: ' + (log.getValue('u_metadata') || 'N/A'));
var snapshot = log.getValue('u_content_snapshot');
if (snapshot) {
output.push(' Content:');
var snapLines = snapshot.split('\n');
for (var sl = 0; sl < snapLines.length && sl < 10; sl++) {
output.push(' ' + snapLines[sl]);
}
if (snapLines.length > 10) {
output.push(' [... ' + (snapLines.length - 10) +
' more lines truncated]');
}
}
output.push('');
}
output.push('Total evidence entries: ' + count);
output.push('═══════════════════════════════════════════════════════');
return output.join('\n');
},
/*
* Verify evidence chain integrity.
* Re-computes hashes and checks for gaps or tampering.
*/
verifyIntegrity: function(reportSysId) {
var log = new GlideRecord('u_nis2_evidence_log');
log.addQuery('u_nis2_report', reportSysId);
log.orderBy('u_action_datetime');
log.query();
var issues = [];
var lastTimestamp = null;
var count = 0;
while (log.next()) {
count++;
// Check hash integrity
var storedHash = log.getValue('u_content_hash');
var snapshot = log.getValue('u_content_snapshot');
if (storedHash && snapshot) {
// ⚠️ VERIFY: confirm class exists in your SN release and scope [GlideDigest]
var recomputedHash = new GlideDigest().getSHA256Hex(snapshot);
if (recomputedHash !== storedHash) {
issues.push('TAMPER DETECTED: Event #' + count +
' hash mismatch. Stored: ' + storedHash +
' Computed: ' + recomputedHash);
}
}
// Check for timeline gaps > 24 hours (potential missing events)
var currentTimestamp = log.getValue('u_action_datetime');
if (lastTimestamp) {
var gap = new GlideDateTime(currentTimestamp);
var prev = new GlideDateTime(lastTimestamp);
var diffMs = GlideDateTime.subtract(prev, gap).getNumericValue();
var diffHours = diffMs / (1000 * 60 * 60);
if (diffHours > 24) {
issues.push('TIMELINE GAP: ' + Math.round(diffHours) +
'h gap between events #' + (count - 1) +
' and #' + count);
}
}
lastTimestamp = currentTimestamp;
}
return {
totalEvents: count,
issues: issues,
isClean: issues.length === 0
};
},
type: 'NIS2EvidenceTrail'
};
Business Rule: Track All Field Changes on NIS2 Report
/*
* NIS2-23 Business Rule: Evidence Trail for NIS2 Report Changes
*
* Logs every field change on u_nis2_incident_report to the evidence trail.
* This provides regulatory proof of when decisions were made.
*
* Table: u_nis2_incident_report
* When: After Update
* Condition: (none — runs on every update)
* Order: 500
*
* Versions: Washington+
*/
(function executeRule(current, previous) {
// ⚠️ VERIFY: confirm class exists in your SN release and scope [NIS2EvidenceTrail]
var evidence = new NIS2EvidenceTrail();
// Track changes on all reporting-relevant fields
var trackedFields = [
'u_nis2_classification',
'u_suspected_malicious',
'u_cross_border_impact',
'u_member_states_affected',
'u_early_warning_status',
'u_notification_status',
'u_final_report_status',
'u_severity_assessment',
'u_estimated_affected_users',
'u_impact_description',
'u_ioc_summary',
'u_root_cause',
'u_remediation_actions',
'u_lessons_learned',
'u_csirt_reference',
'state',
'assignment_group',
'assigned_to'
];
for (var i = 0; i < trackedFields.length; i++) {
evidence.logFieldChange(current, previous, trackedFields[i]);
}
// Special handling for status transitions — these are the key regulatory events
if (current.getValue('u_early_warning_status') !== previous.getValue('u_early_warning_status') &&
current.getValue('u_early_warning_status') === 'submitted') {
evidence.logAction(
current.getUniqueValue(),
'early_warning_submitted',
'Early warning marked as submitted at ' +
current.getDisplayValue('u_early_warning_submitted'),
'SLA clock reference: u_awareness_datetime = ' +
current.getDisplayValue('u_awareness_datetime')
);
}
if (current.getValue('u_notification_status') !== previous.getValue('u_notification_status') &&
current.getValue('u_notification_status') === 'submitted') {
evidence.logAction(
current.getUniqueValue(),
'notification_submitted',
'72h notification marked as submitted at ' +
current.getDisplayValue('u_notification_submitted'),
'CSIRT reference: ' + (current.getValue('u_csirt_reference') || 'N/A')
);
}
if (current.getValue('u_final_report_status') !== previous.getValue('u_final_report_status') &&
current.getValue('u_final_report_status') === 'submitted') {
evidence.logAction(
current.getUniqueValue(),
'final_report_submitted',
'Final report marked as submitted at ' +
current.getDisplayValue('u_final_report_submitted'),
'CSIRT reference: ' + (current.getValue('u_csirt_reference') || 'N/A')
);
}
})(current, previous);
Dashboard Widget: Active NIS2-Reportable Incidents and SLA Status
/*
* NIS2-23 Widget: NIS2 Incident Reporting Dashboard
*
* Server script for a Service Portal widget or Performance Analytics indicator
* that shows all active NIS2-reportable incidents with their SLA status.
*
* Deploy as:
* - Service Portal widget (server script below, HTML/CSS separate)
* - Performance Analytics indicator/dashboard
* - Scheduled report emailed to the NIS2 compliance team
*
* Versions: Washington+
*/
// --- SERVER SCRIPT (for Service Portal widget) ---
(function() {
var reports = [];
var gr = new GlideRecord('u_nis2_incident_report');
gr.addQuery('u_nis2_classification', 'significant');
gr.addEncodedQuery(
'u_early_warning_status!=submitted^OR' +
'u_notification_status!=submitted^OR' +
'u_final_report_status!=submitted'
);
gr.orderByDesc('u_awareness_datetime');
gr.query();
while (gr.next()) {
var report = {};
report.sys_id = gr.getUniqueValue();
report.number = gr.getValue('number');
report.short_description = gr.getValue('short_description');
report.awareness_datetime = gr.getDisplayValue('u_awareness_datetime');
report.classification = gr.getValue('u_nis2_classification');
report.severity = gr.getDisplayValue('u_severity_assessment');
report.cross_border = gr.getValue('u_cross_border_impact') === '1';
// Early warning status and SLA
report.ew_status = gr.getValue('u_early_warning_status');
report.ew_submitted = gr.getDisplayValue('u_early_warning_submitted');
// 72h notification status
report.notif_status = gr.getValue('u_notification_status');
report.notif_submitted = gr.getDisplayValue('u_notification_submitted');
// Final report status
report.final_status = gr.getValue('u_final_report_status');
report.final_submitted = gr.getDisplayValue('u_final_report_submitted');
// Calculate time remaining for each SLA
var awareness = new GlideDateTime(gr.getValue('u_awareness_datetime'));
var now = new GlideDateTime();
var elapsedMs = GlideDateTime.subtract(awareness, now).getNumericValue();
var elapsedHours = elapsedMs / (1000 * 60 * 60);
// 24h early warning
if (report.ew_status === 'pending') {
report.ew_remaining_hours = Math.max(0, 24 - elapsedHours);
report.ew_percentage = Math.min(100,
Math.round((elapsedHours / 24) * 100));
report.ew_overdue = elapsedHours > 24;
}
// 72h notification
if (report.notif_status === 'pending') {
report.notif_remaining_hours = Math.max(0, 72 - elapsedHours);
report.notif_percentage = Math.min(100,
Math.round((elapsedHours / 72) * 100));
report.notif_overdue = elapsedHours > 72;
}
// 30d final report
if (report.final_status === 'pending') {
var thirtyDaysHours = 30 * 24;
report.final_remaining_days = Math.max(0,
Math.round((thirtyDaysHours - elapsedHours) / 24));
report.final_percentage = Math.min(100,
Math.round((elapsedHours / thirtyDaysHours) * 100));
report.final_overdue = elapsedHours > thirtyDaysHours;
}
// Get linked security incident number
var secInc = new GlideRecord('sn_si_incident');
if (secInc.get(gr.getValue('u_security_incident'))) {
report.sec_incident_number = secInc.getValue('number');
report.sec_incident_priority = secInc.getDisplayValue('priority');
}
reports.push(report);
}
data.reports = reports;
data.total_active = reports.length;
// Summary counts
data.ew_overdue = reports.filter(function(r) { return r.ew_overdue; }).length;
data.notif_overdue = reports.filter(function(r) { return r.notif_overdue; }).length;
data.final_overdue = reports.filter(function(r) { return r.final_overdue; }).length;
data.total_overdue = data.ew_overdue + data.notif_overdue + data.final_overdue;
})();
// --- HTML TEMPLATE (for Service Portal widget) ---
/*
<div class="nis2-dashboard">
<h2>NIS2 Art.23 Incident Reporting Status</h2>
<div class="summary-cards">
<div class="card" ng-class="{'card-danger': data.total_overdue > 0}">
<span class="count">{{data.total_active}}</span>
<span class="label">Active Reports</span>
</div>
<div class="card card-danger" ng-if="data.total_overdue > 0">
<span class="count">{{data.total_overdue}}</span>
<span class="label">Overdue SLAs</span>
</div>
</div>
<table class="table table-hover" ng-if="data.reports.length > 0">
<thead>
<tr>
<th>Report</th>
<th>Security Incident</th>
<th>Severity</th>
<th>24h Early Warning</th>
<th>72h Notification</th>
<th>30d Final Report</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="r in data.reports">
<td>
<a href="?id=form&table=u_nis2_incident_report&sys_id={{r.sys_id}}">
{{r.number}}
</a>
</td>
<td>{{r.sec_incident_number}} ({{r.sec_incident_priority}})</td>
<td>{{r.severity}}</td>
<td>
<span ng-if="r.ew_status == 'submitted'" class="status-ok">
Submitted {{r.ew_submitted}}
</span>
<span ng-if="r.ew_status == 'pending' && !r.ew_overdue"
class="status-warn">
{{r.ew_remaining_hours | number:1}}h remaining
({{r.ew_percentage}}%)
</span>
<span ng-if="r.ew_overdue" class="status-danger">
OVERDUE
</span>
</td>
<td>
<span ng-if="r.notif_status == 'submitted'" class="status-ok">
Submitted {{r.notif_submitted}}
</span>
<span ng-if="r.notif_status == 'pending' && !r.notif_overdue"
class="status-warn">
{{r.notif_remaining_hours | number:1}}h remaining
({{r.notif_percentage}}%)
</span>
<span ng-if="r.notif_overdue" class="status-danger">
OVERDUE
</span>
</td>
<td>
<span ng-if="r.final_status == 'submitted'" class="status-ok">
Submitted {{r.final_submitted}}
</span>
<span ng-if="r.final_status == 'pending' && !r.final_overdue"
class="status-warn">
{{r.final_remaining_days}}d remaining
({{r.final_percentage}}%)
</span>
<span ng-if="r.final_overdue" class="status-danger">
OVERDUE
</span>
</td>
</tr>
</tbody>
</table>
<p ng-if="data.reports.length == 0">
No active NIS2-reportable incidents. All reporting obligations are current.
</p>
</div>
*/
// --- CSS (for Service Portal widget) ---
/*
.nis2-dashboard { padding: 16px; }
.summary-cards { display: flex; gap: 16px; margin-bottom: 24px; }
.card { background: #1e293b; border-radius: 8px; padding: 20px; text-align: center; min-width: 150px; }
.card .count { display: block; font-size: 36px; font-weight: bold; color: #38bdf8; }
.card .label { display: block; font-size: 14px; color: #94a3b8; margin-top: 4px; }
.card-danger { border: 2px solid #ef4444; }
.card-danger .count { color: #ef4444; }
.status-ok { color: #22c55e; font-weight: bold; }
.status-warn { color: #f59e0b; font-weight: bold; }
.status-danger { color: #ef4444; font-weight: bold; animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
*/
Scheduled Job: Daily NIS2 SLA Status Check
/*
* NIS2-23 Scheduled Job: Daily NIS2 SLA Status Check
*
* Runs daily and checks all active NIS2 reports for approaching deadlines.
* Sends summary to the NIS2 compliance team.
*
* Deploy as: Scheduled Script Execution (sysauto_script)
* Schedule: Daily at 08:00
*
* Versions: Washington+
*/
gs.info('=== NIS2-23: DAILY SLA STATUS CHECK ===');
gs.info('Check time: ' + new GlideDateTime().getDisplayValue());
gs.info('');
var now = new GlideDateTime();
var actionRequired = [];
var allClear = true;
var gr = new GlideRecord('u_nis2_incident_report');
gr.addQuery('u_nis2_classification', 'significant');
gr.addEncodedQuery(
'u_early_warning_status=pending^OR' +
'u_notification_status=pending^OR' +
'u_final_report_status=pending'
);
gr.query();
while (gr.next()) {
var awareness = new GlideDateTime(gr.getValue('u_awareness_datetime'));
var elapsedMs = GlideDateTime.subtract(awareness, now).getNumericValue();
var elapsedHours = elapsedMs / (1000 * 60 * 60);
var reportNum = gr.getValue('number');
// Check 24h early warning
if (gr.getValue('u_early_warning_status') === 'pending') {
var ewRemaining = 24 - elapsedHours;
if (ewRemaining <= 0) {
actionRequired.push('[BREACHED] ' + reportNum +
': 24h early warning OVERDUE by ' +
Math.round(Math.abs(ewRemaining)) + 'h');
gr.setValue('u_early_warning_status', 'overdue');
gr.update();
allClear = false;
} else if (ewRemaining <= 4) {
actionRequired.push('[URGENT] ' + reportNum +
': 24h early warning due in ' +
Math.round(ewRemaining) + 'h');
allClear = false;
}
}
// Check 72h notification
if (gr.getValue('u_notification_status') === 'pending') {
var notifRemaining = 72 - elapsedHours;
if (notifRemaining <= 0) {
actionRequired.push('[BREACHED] ' + reportNum +
': 72h notification OVERDUE by ' +
Math.round(Math.abs(notifRemaining)) + 'h');
gr.setValue('u_notification_status', 'overdue');
gr.update();
allClear = false;
} else if (notifRemaining <= 12) {
actionRequired.push('[URGENT] ' + reportNum +
': 72h notification due in ' +
Math.round(notifRemaining) + 'h');
allClear = false;
}
}
// Check 30d final report
if (gr.getValue('u_final_report_status') === 'pending') {
var finalRemainingDays = (30 * 24 - elapsedHours) / 24;
if (finalRemainingDays <= 0) {
actionRequired.push('[BREACHED] ' + reportNum +
': 30d final report OVERDUE by ' +
Math.round(Math.abs(finalRemainingDays)) + ' days');
gr.setValue('u_final_report_status', 'overdue');
gr.update();
allClear = false;
} else if (finalRemainingDays <= 5) {
actionRequired.push('[WARNING] ' + reportNum +
': 30d final report due in ' +
Math.round(finalRemainingDays) + ' days');
allClear = false;
}
}
}
if (allClear) {
gs.info('All NIS2 reporting obligations are current. No action required.');
} else {
gs.info('ACTION REQUIRED:');
for (var i = 0; i < actionRequired.length; i++) {
gs.info(' ' + actionRequired[i]);
}
gs.info('');
gs.info('Total items requiring attention: ' + actionRequired.length);
// Send notification to NIS2 compliance team
var complianceGroup = gs.getProperty('u_nis2.compliance_group',
'NIS2 Compliance Team');
gs.eventQueue('u_nis2.daily_sla_summary', null,
actionRequired.join('\n'), complianceGroup);
}
Email Notification Templates for Each Stage
/*
* NIS2-23 Notification Records
*
* Create these notification records in ServiceNow:
* System Notification > Email > Notifications
*
* Each notification triggers on the corresponding event.
* Below are the mail scripts for each notification.
*
* Versions: Washington+
*/
// ===================================================================
// NOTIFICATION 1: NIS2 Significant Incident Detected
// Event: u_nis2.significant_incident_detected
// Recipients: NIS2 Compliance Team group
// ===================================================================
/*
* Mail Script: nis2_significant_incident_detected
*
* Subject: [NIS2 ALERT] Significant Incident Classified — {{event.parm1}}
*/
// Body mail script:
(function() {
var report = new GlideRecord('u_nis2_incident_report');
report.get(event.instance);
template.print('<h2>NIS2 Art.23 — Significant Incident Detected</h2>');
template.print('<p><strong>A security incident has been automatically ');
template.print('classified as NIS2-reportable.</strong></p>');
template.print('<table border="1" cellpadding="8">');
template.print('<tr><td>NIS2 Report</td><td>' +
report.getValue('number') + '</td></tr>');
template.print('<tr><td>Security Incident</td><td>' +
event.parm1 + '</td></tr>');
template.print('<tr><td>Classification Triggers</td><td>' +
event.parm2 + '</td></tr>');
template.print('<tr><td>Awareness Time</td><td>' +
report.getDisplayValue('u_awareness_datetime') + '</td></tr>');
template.print('<tr><td>24h Deadline</td><td><strong>' +
'SUBMIT EARLY WARNING WITHIN 24 HOURS</strong></td></tr>');
template.print('</table>');
template.print('<p>Immediate actions required:</p>');
template.print('<ol>');
template.print('<li>Review the classification and confirm significance</li>');
template.print('<li>Assess whether the incident is malicious</li>');
template.print('<li>Assess cross-border impact</li>');
template.print('<li>Prepare and submit early warning to CSIRT</li>');
template.print('</ol>');
})();
// ===================================================================
// NOTIFICATION 2: SLA at 50% / 75%
// Event: u_nis2.sla_50_percent / u_nis2.sla_75_percent
// Recipients: Assigned group + NIS2 Compliance Lead
// ===================================================================
// Body mail script (75% — critical version):
(function() {
var report = new GlideRecord('u_nis2_incident_report');
report.get(event.instance);
template.print('<h2 style="color: #f59e0b;">NIS2 DEADLINE WARNING — 75%</h2>');
template.print('<p>The following NIS2 reporting deadline has consumed ');
template.print('75% of the available time:</p>');
template.print('<table border="1" cellpadding="8">');
template.print('<tr><td>Deadline Type</td><td><strong>' +
event.parm1 + '</strong></td></tr>');
template.print('<tr><td>NIS2 Report</td><td>' +
event.parm2 + '</td></tr>');
template.print('<tr><td>Awareness Time</td><td>' +
report.getDisplayValue('u_awareness_datetime') + '</td></tr>');
template.print('</table>');
template.print('<p style="color: #f59e0b;"><strong>');
template.print('Action required immediately to avoid regulatory breach.');
template.print('</strong></p>');
})();
// ===================================================================
// NOTIFICATION 3: SLA Breached
// Event: u_nis2.sla_breached
// Recipients: CISO + NIS2 Compliance Lead + Legal
// ===================================================================
// Body mail script:
(function() {
var report = new GlideRecord('u_nis2_incident_report');
report.get(event.instance);
template.print('<h2 style="color: #ef4444;">NIS2 DEADLINE BREACHED</h2>');
template.print('<p style="color: #ef4444;"><strong>');
template.print('A mandatory NIS2 Art.23 reporting deadline has been missed.');
template.print('</strong></p>');
template.print('<table border="1" cellpadding="8">');
template.print('<tr><td>Breached Deadline</td><td><strong>' +
event.parm1 + '</strong></td></tr>');
template.print('<tr><td>NIS2 Report</td><td>' +
event.parm2 + '</td></tr>');
template.print('<tr><td>Awareness Time</td><td>' +
report.getDisplayValue('u_awareness_datetime') + '</td></tr>');
template.print('</table>');
template.print('<p>This constitutes a regulatory non-compliance event ');
template.print('under NIS2 Art.23. Potential consequences:</p>');
template.print('<ul>');
template.print('<li>Fines up to EUR 10,000,000 or 2% of global ');
template.print('annual turnover</li>');
template.print('<li>Supervisory action by the competent authority</li>');
template.print('<li>Personal liability for management body members ');
template.print('(Art.20§1)</li>');
template.print('</ul>');
template.print('<p><strong>Submit the notification to the CSIRT ');
template.print('IMMEDIATELY and document the reason for delay.</strong></p>');
})();
Integration with CSIRT
Organizations have three options for submitting NIS2 notifications to their national CSIRT, ranging from simple to fully automated.
Option 1: Manual Process (Minimum Viable)
This is the simplest approach and what most organizations will start with. Use the notification templates above to generate the content, then submit through the CSIRT's designated channel.
MANUAL CSIRT SUBMISSION PROCESS:
1. Generate the notification content:
- Navigate to the u_nis2_incident_report record
- Click "Generate Early Warning" / "Generate Notification" UI action
- Copy the generated content
2. Submit via CSIRT-designated channel:
- Most EU CSIRTs provide a web portal for submission
- Some accept email to a dedicated address
- Some provide a phone hotline for initial early warnings
3. Record the submission:
- Enter the CSIRT reference number in u_csirt_reference
- Update the stage status to "submitted"
- The evidence trail will automatically log the submission
EU CSIRT CONTACT POINTS (by Member State):
(Consult your national CSIRT for the correct submission method)
- ENISA maintains a list: https://csirts-network.eu/
- Most CSIRTs have updated their portals for NIS2 reporting
- The submission method may change as Member States finalize
their NIS2 transposition
Option 2: Email Notification (Semi-Automated)
Configure ServiceNow to automatically generate and email the notification content to the CSIRT's designated email address. The NIS2 response team reviews the draft before it is sent.
/*
* NIS2-23 UI Action: Generate and Email Early Warning to CSIRT
*
* Table: u_nis2_incident_report
* Action name: Send Early Warning to CSIRT
* Show insert: false
* Show update: true
* Condition: current.u_early_warning_status == 'pending' &&
* current.u_nis2_classification == 'significant'
*
* Versions: Washington+
*/
(function() {
// Generate the early warning content
// ⚠️ VERIFY: confirm class exists in your SN release and scope [NIS2EarlyWarningTemplate]
var template = new NIS2EarlyWarningTemplate(current.getUniqueValue());
var content = template.generate();
// Log evidence BEFORE sending
// ⚠️ VERIFY: confirm class exists in your SN release and scope [NIS2EvidenceTrail]
var evidence = new NIS2EvidenceTrail();
evidence.logAction(
current.getUniqueValue(),
'early_warning_generated',
content,
'Generated for CSIRT submission via email'
);
// Create email record
var email = new GlideRecord('sys_email');
email.initialize();
email.setValue('type', 'send-ready');
email.setValue('recipients',
gs.getProperty('u_nis2.csirt_email', 'csirt@example.eu'));
email.setValue('copy',
gs.getProperty('u_nis2.contact_email', ''));
email.setValue('subject',
'[NIS2 Early Warning] ' + current.getValue('number') +
' — ' + current.getValue('short_description'));
email.setValue('body', content);
email.setValue('importance', 'high');
email.insert();
// Mark as submitted
template.markSubmitted();
// Log the send
evidence.logAction(
current.getUniqueValue(),
'early_warning_submitted',
'Email sent to: ' + gs.getProperty('u_nis2.csirt_email', 'csirt@example.eu'),
'Email sys_id: ' + email.getUniqueValue()
);
gs.addInfoMessage('Early warning sent to CSIRT. Update the CSIRT reference ' +
'number when received.');
})();
Option 3: API Integration (Fully Automated)
Some CSIRTs provide REST APIs for notification submission. This is the most mature approach — notifications are submitted programmatically with full traceability.
/*
* NIS2-23 Script Include: CSIRT API Integration
*
* Submits NIS2 notifications to the CSIRT REST API.
* This is a template — adapt the endpoint, authentication, and payload
* format to your national CSIRT's API specification.
*
* Versions: Washington+
*/
var NIS2CSIRTIntegration = Class.create();
NIS2CSIRTIntegration.prototype = {
initialize: function() {
this.apiEndpoint = gs.getProperty('u_nis2.csirt_api_endpoint', '');
this.apiKey = gs.getProperty('u_nis2.csirt_api_key', '');
this.orgId = gs.getProperty('u_nis2.csirt_org_id', '');
},
/*
* Submit a notification to the CSIRT API.
*
* @param {string} reportSysId — sys_id of the NIS2 report
* @param {string} stage — 'early_warning', 'notification', or 'final_report'
* @param {string} content — the notification content
* @returns {object} — { success: bool, reference: string, error: string }
*/
submit: function(reportSysId, stage, content) {
if (!this.apiEndpoint) {
return {
success: false,
reference: null,
error: 'CSIRT API endpoint not configured. ' +
'Set u_nis2.csirt_api_endpoint system property.'
};
}
var report = new GlideRecord('u_nis2_incident_report');
if (!report.get(reportSysId)) {
return {
success: false,
reference: null,
error: 'NIS2 report not found: ' + reportSysId
};
}
// Build API payload — adapt to your CSIRT's API schema
var payload = {
organization_id: this.orgId,
notification_type: stage,
notification_stage: this._mapStageToAPI(stage),
internal_reference: report.getValue('number'),
awareness_datetime: report.getValue('u_awareness_datetime'),
suspected_malicious: report.getValue('u_suspected_malicious') === '1',
cross_border_impact: report.getValue('u_cross_border_impact') === '1',
member_states_affected: report.getValue('u_member_states_affected') || '',
severity: report.getValue('u_severity_assessment') || 'under_assessment',
content: content,
submission_datetime: new GlideDateTime().getValue()
};
// Log evidence before submission attempt
// ⚠️ VERIFY: confirm class exists in your SN release and scope [NIS2EvidenceTrail]
var evidence = new NIS2EvidenceTrail();
evidence.logAction(
reportSysId,
stage + '_api_submission_attempt',
JSON.stringify(payload),
'Endpoint: ' + this.apiEndpoint
);
// Make the API call
try {
var restMessage = new sn_ws.RESTMessageV2();
restMessage.setEndpoint(this.apiEndpoint);
restMessage.setHttpMethod('POST');
restMessage.setRequestHeader('Content-Type', 'application/json');
restMessage.setRequestHeader('Authorization', 'Bearer ' + this.apiKey);
restMessage.setRequestHeader('X-NIS2-Organization', this.orgId);
restMessage.setRequestBody(JSON.stringify(payload));
restMessage.setHttpTimeout(30000); // 30 second timeout
var response = restMessage.execute();
var httpStatus = response.getStatusCode();
var responseBody = response.getBody();
if (httpStatus >= 200 && httpStatus < 300) {
var responseObj = JSON.parse(responseBody);
var csirtRef = responseObj.reference_number ||
responseObj.id || 'API-' + httpStatus;
// Log successful submission
evidence.logAction(
reportSysId,
stage + '_api_submission_success',
responseBody,
'HTTP ' + httpStatus + ' | CSIRT Ref: ' + csirtRef
);
return {
success: true,
reference: csirtRef,
error: null
};
} else {
// Log failed submission
evidence.logAction(
reportSysId,
stage + '_api_submission_failed',
responseBody,
'HTTP ' + httpStatus + ' — submission failed'
);
return {
success: false,
reference: null,
error: 'CSIRT API returned HTTP ' + httpStatus +
': ' + responseBody
};
}
} catch (e) {
evidence.logAction(
reportSysId,
stage + '_api_submission_error',
e.getMessage(),
'Exception during API call'
);
return {
success: false,
reference: null,
error: 'API call failed: ' + e.getMessage()
};
}
},
_mapStageToAPI: function(stage) {
var stageMap = {
'early_warning': 'EARLY_WARNING_24H',
'notification': 'INCIDENT_NOTIFICATION_72H',
'final_report': 'FINAL_REPORT_30D',
'progress_report': 'PROGRESS_REPORT'
};
return stageMap[stage] || stage;
},
type: 'NIS2CSIRTIntegration'
};
CSIRT Submission Template
For CSIRTs that accept structured submissions (web form or email), use this template as a starting point. Adapt it to the specific format required by your national CSIRT.
═══════════════════════════════════════════════════════════════
CSIRT SUBMISSION TEMPLATE — NIS2 ART.23 INCIDENT NOTIFICATION
═══════════════════════════════════════════════════════════════
SUBMISSION TYPE: [ ] Early Warning (24h)
[ ] Incident Notification (72h)
[ ] Final Report (30d)
[ ] Progress Report (ongoing)
[ ] Intermediate Report (on request)
REPORTING ENTITY
Legal name: ____________________________________
NIS2 entity type: [ ] Essential [ ] Important
Sector: ____________________________________
Sub-sector: ____________________________________
Member State: ____________________________________
Registration/VAT: ____________________________________
CONTACT INFORMATION
Name: ____________________________________
Role: ____________________________________
Email: ____________________________________
Phone: ____________________________________
Backup contact: ____________________________________
INCIDENT DETAILS
Internal reference: ____________________________________
CSIRT reference: ____________________________________ (if updating)
Date/time of awareness: ________________________________
Date/time of incident (if known): ______________________
EARLY WARNING FIELDS (24h)
Suspected unlawful/malicious act: [ ] Yes [ ] No [ ] Unknown
Cross-border impact possible: [ ] Yes [ ] No [ ] Unknown
Affected Member States: ____________________________________
Initial description:
________________________________________________________________
________________________________________________________________
IMPACT ASSESSMENT (72h update)
Severity: [ ] Critical [ ] High [ ] Medium [ ] Low
Affected users/entities: ____________________________________
Affected services: ____________________________________
Duration (so far): ____________________________________
Data categories affected: ____________________________________
Indicators of compromise:
________________________________________________________________
________________________________________________________________
ROOT CAUSE AND REMEDIATION (Final report)
Root cause:
________________________________________________________________
________________________________________________________________
Threat type: [ ] Exploitation [ ] Ransomware [ ] Phishing
[ ] DDoS [ ] Insider [ ] Supply chain [ ] Other
Mitigation measures:
________________________________________________________________
________________________________________________________________
Ongoing measures:
________________________________________________________________
Lessons learned:
________________________________________________________________
________________________________________________________________
CONCURRENT NOTIFICATIONS
GDPR Art.33 notification submitted: [ ] Yes [ ] No [ ] N/A
DORA Art.17 notification submitted: [ ] Yes [ ] No [ ] N/A
Other sector-specific notifications: ____________________________________
ATTACHMENTS
[ ] IOC list (CSV/STIX format)
[ ] Timeline document
[ ] Impact assessment report
[ ] Evidence chain export
Signature: ____________________________ Date: ________________
═══════════════════════════════════════════════════════════════
Regulatory Impact
NIS2 Mapping
| Article | Requirement | How This Implementation Addresses It | Evidence |
|---|---|---|---|
| Art.23§1 | Notify CSIRT of significant incidents without undue delay | Automated classification triggers NIS2 report creation with SLA timers starting immediately upon awareness | u_nis2_incident_report record creation timestamp, evidence trail log |
| Art.23§3 | Definition of "significant incident" | Classification business rule evaluates all Art.23§3 criteria against security incident data | u_trigger_criteria field documents which criteria were met |
| Art.23§4(a) | 24-hour early warning | SLA definition enforces 24h deadline; template generates compliant content; escalation at 50%/75%/breach | SLA records, evidence trail, email notification logs |
| Art.23§4(b) | 72-hour incident notification | SLA definition enforces 72h deadline; comprehensive template includes severity, IOCs, affected services | SLA records, evidence trail, notification content hash |
| Art.23§4(c) | Intermediate report on request | Progress report function available on the NIS2 report record | u_final_report_status = progress_report_submitted |
| Art.23§4(d) | 1-month final report | SLA definition enforces 30d deadline; template includes root cause, remediation, cross-border impact | SLA records, evidence trail, final report content hash |
| Art.23§4(e) | Progress report if ongoing at 1 month | submitProgressReport() function marks ongoing status and resets final report deadline |
Evidence trail log |
| Art.23§5 | Cross-border notification coordination | u_cross_border_impact and u_member_states_affected fields included in all templates |
CSIRT submission records |
| Art.30 | Voluntary notifications | Same workflow can be used for voluntary reporting of near-misses | Classification field supports manual override |
Cross-Reference: DORA Art.17 (Financial Sector)
DORA imposes similar but not identical incident reporting requirements on financial entities. Key differences from NIS2:
| Aspect | NIS2 Art.23 | DORA Art.17 |
|---|---|---|
| Scope | All essential and important entities | Financial entities (banks, insurers, investment firms, etc.) |
| Classification criteria | Severe disruption, affected other persons, cross-border | Number of clients, duration, geographical spread, data losses, criticality, economic impact |
| Early notification | 24 hours | Initial notification "without undue delay" |
| Full notification | 72 hours | Intermediate report within defined timeframe |
| Final report | 1 month | Final report within defined timeframe |
| Recipient | National CSIRT | Competent financial authority (ECB, national regulator) |
| Format | To be defined by implementing acts | Standardized templates per ESA guidelines |
If your organization falls under both NIS2 and DORA (e.g., a bank), the u_dora_notification_req flag on the NIS2 report record ensures both notification streams are triggered. The content templates can be extended to include DORA-specific classification criteria.
Cross-Reference: GDPR Art.33 (Data Breach Notification)
GDPR Art.33 requires notification to the Data Protection Authority within 72 hours of becoming aware of a personal data breach. Key overlap with NIS2:
| Aspect | NIS2 Art.23 | GDPR Art.33 |
|---|---|---|
| Trigger | Significant incident (any type) | Personal data breach specifically |
| Deadline | 24h early warning + 72h full notification | 72 hours (single notification) |
| Recipient | National CSIRT | Data Protection Authority |
| Content | Severity, IOCs, affected services | Nature of breach, categories of data subjects, DPO contact, consequences, measures |
| Data subject notification | Not required directly | Required if high risk (Art.34) |
Most NIS2-reportable security incidents involving ServiceNow will also trigger GDPR Art.33, because ServiceNow instances almost always contain personal data (sys_user at minimum). The u_gdpr_notification_req flag ensures this parallel obligation is tracked.
Expert Notes
NIS2 Article 23 — Incident Reporting Workflow Implementation differs from posture-scan articles like Empty Condition ACLs on Sensitive Tables or Insecure System Properties Exposing Secrets: it is an implementation guide, not a detection script collection. The deliverable is a working incident-reporting workflow on the customer's instance, not a compliance scan result. Use the article as a build runbook.
The single most-overlooked NIS2 Article 23 — Incident Reporting Workflow Implementation design decision: the
u_awareness_datetimefield starts ALL three SLA clocks (24h / 72h / 1-month). Auditors will challenge any gap between the Incident Response Playbook for Critical CVE Exploitation detection timestamp and the awareness datetime. Architect the business rule so that creating a "significant" classification automatically copies the source security incident's earliest detection timestamp intou_awareness_datetime— and prevents manual override without an audit-logged justification.Customers subject to both NIS2 and DORA must run a single workflow with the
u_dora_notification_reqflag, not two parallel workflows. Two parallel workflows produce timeline drift and contradictory submissions. The same applies to GDPR Art.33 —u_gdpr_notification_reqflag triggers the 72-hour DPA notification within the same record.