← All security articles
CRITICALZurich Patch 6 (review-validated 2026-04-28); table-design and business-rule patterns are platform-version-stable

NIS2 Article 23 — Incident Reporting Workflow Implementation

Domain 6: Regulatory Compliance·

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:

  1. Flag the cross-border nature in your early warning
  2. Identify which Member States may be affected
  3. Describe the nature of the cross-border impact
  4. 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:

  1. 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.
  2. Whether the incident could have cross-border impact — again, a yes/no assessment based on your initial understanding.
  3. 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:

  1. Update to the early warning — any changes to the malicious/cross-border assessment
  2. Initial assessment of the incident — severity, impact scope, and nature
  3. Indicators of compromise — where available at this stage
  4. 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:

  1. Detailed description of the incident — including severity, scope, and timeline
  2. Type of threat or root cause — that likely triggered the incident
  3. Applied and ongoing mitigation measures — what was done and what is still being done
  4. 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_datetime field 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 into u_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_req flag, not two parallel workflows. Two parallel workflows produce timeline drift and contradictory submissions. The same applies to GDPR Art.33 — u_gdpr_notification_req flag triggers the 72-hour DPA notification within the same record.