2026-04-16·15 min read·

CRA Art.14/16: Vulnerability Reporting to ENISA — 24-Hour Notification, CVD Policy, and the September 2026 Deadline

On 11 September 2026, the vulnerability reporting provisions of the EU Cyber Resilience Act (Regulation (EU) 2024/2847, "CRA") begin to apply. Any manufacturer placing a product with digital elements on the EU market — software, firmware, connected hardware, SaaS — must from that date:

  1. Notify ENISA of any actively exploited vulnerability within 24 hours (early warning)
  2. Submit a full notification within 72 hours with mitigation status
  3. Deliver a final report within 14 days after remediation
  4. Operate and publish a Coordinated Vulnerability Disclosure (CVD) policy

These are not aspirational guidelines. They are directly applicable legal obligations backed by the same penalty framework as the rest of the CRA: up to €15 million or 2.5% of worldwide annual turnover for non-compliance.

The CRA SBOM and vulnerability handling requirements (Art.13) that apply from December 2027 are widely discussed. The September 2026 reporting obligations are less understood — and the implementation clock is now under five months.

What Triggers the September 2026 Date?

The CRA entered into force on 10 December 2024. Art. 71(2) provides that Articles 14 and 15 — notification of actively exploited vulnerabilities and CVD obligations — apply from 21 months after entry into force. 21 months from 10 December 2024 = 11 September 2026.

The full CRA manufacturer obligations (security-by-design, SBOM, security updates) apply from 11 December 2027 (36 months). The notification provisions are an early subset deliberately brought forward to operationalise the ENISA vulnerability monitoring system before full enforcement.

Timeline summary:

If your organisation is planning a single CRA compliance sprint for "late 2027", the September 2026 notification obligations will hit first — and without preparation, they will hit hard.

Scope: Who Must Report to ENISA?

Art.14 reporting obligations apply to manufacturers — defined in Art.3(13) as any natural or legal person that develops or manufactures products with digital elements or has such products designed, developed, or manufactured and markets them under their name or trademark. This covers:

Not covered by Art.14:

The scope question is often where organisations get it wrong. If your SaaS product is the primary commercial offering — not just ancillary digital processing inside a physical product — you are a manufacturer under the CRA.

The 3-Stage ENISA Notification Process (Art.14)

Stage 1: 24-Hour Early Warning

Within 24 hours of becoming aware of an actively exploited vulnerability in your product, you must submit an early warning to ENISA via the Single Reporting Platform (SRP) specified in Art.14(8).

What is "actively exploited"? The CRA does not define this with the precision that security practitioners would prefer. The operative criterion is that the vulnerability is being exploited in the wild — not merely that a proof-of-concept exists. Indicators include:

The 24-hour clock starts at manufacturer awareness — when your security team or monitoring systems first identify or receive credible notification that the vulnerability is being actively exploited. Building a documented awareness-and-escalation process is therefore a prerequisite: you need to prove when you became aware.

Early warning content (Art.14(3)): The early warning requires only basic information:

Stage 2: 72-Hour Full Notification

Within 72 hours of becoming aware, you must submit a full notification covering:

The 72-hour window is identical to NIS2 Art.23's significant incident notification timeline — this alignment is intentional. See the dual-reporting section below.

Stage 3: Final Report

Within 14 days of deploying a fix or mitigation, you must submit a final report including:

The final report is where ENISA builds its vulnerability intelligence database. This is the mechanism through which the CRA's notification regime feeds into ENISA's European vulnerability database (Art.12 EUVD, which went live in 2024 as the EU counterpart to NVD).

CVD Policy Obligation (Art.15)

Separately from the ENISA notification obligation, Art.15 requires manufacturers to establish and operate a Coordinated Vulnerability Disclosure policy and make it publicly accessible. This is the formal CVD programme requirement.

A compliant CVD policy under Art.15 must:

  1. Define the scope of products covered
  2. Provide a reporting channel (email, dedicated web form, security.txt)
  3. Specify the response timeline — how quickly you acknowledge receipt and when you commit to deliver a fix or assessment
  4. Define the handling process — triage, severity assessment, fix development, coordinated release
  5. State your disclosure timeline — the period after which you will publicly disclose the vulnerability regardless of patch availability
  6. Describe the coordination process with ENISA for actively exploited vulnerabilities

security.txt (RFC 9116): The practical standard for machine-readable CVD policy publication. CRA compliance does not mandate security.txt by name, but it is the de facto industry standard that ENISA's tooling and automated vulnerability management platforms consume.

# /.well-known/security.txt
Contact: mailto:security@yourcompany.com
Expires: 2027-01-01T00:00:00.000Z
Acknowledgments: https://yourcompany.com/security/hall-of-fame
Policy: https://yourcompany.com/security/cvd-policy
Preferred-Languages: en, de
Encryption: https://yourcompany.com/security/pgp-key.asc

CVD Timeline Standards: The industry baseline is 90 days from researcher notification to public disclosure (Google Project Zero standard), but many EU NCAs and ENISA guidance documents reference 45-90 days. Art.15 does not mandate a specific timeline — it requires that your published policy specifies one, and that you adhere to it.

ENISA Single Reporting Platform (SRP)

Art.14(8) mandates ENISA to establish a Single Reporting Platform for CRA vulnerability notifications. As of April 2026, ENISA has published technical guidance on the SRP and integration options.

Reporting channels:

What ENISA does with notifications:

Notification confidentiality: Art.14(9) provides that information submitted to the SRP may be treated as confidential. ENISA is required to ensure that disclosure of notifications does not compromise product security or enable exploitation. Manufacturers can request that specific technical details remain restricted pending patch availability.

Python Implementation: VulnerabilityReporter

The following Python implementation provides a complete vulnerability tracking and notification pipeline that integrates with the ENISA SRP API spec.

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import json
import hashlib

class ExploitStatus(Enum):
    THEORETICAL = "theoretical"       # PoC exists, no confirmed exploitation
    ACTIVE = "active"                 # Confirmed active exploitation
    WIDESPREAD = "widespread"         # Mass exploitation observed

class NotificationStage(Enum):
    PENDING = "pending"
    EARLY_WARNING_SENT = "early_warning_sent"   # 24h deadline
    FULL_NOTIFICATION_SENT = "full_notification_sent"  # 72h deadline
    FINAL_REPORT_SENT = "final_report_sent"     # 14d after fix

@dataclass
class CRAVulnerability:
    cve_id: str                          # CVE-YYYY-NNNNN or internal ID
    product_name: str
    product_versions: list[str]
    cvss_score: float
    cvss_vector: str
    exploit_status: ExploitStatus
    description: str
    discovered_at: datetime
    reporter: Optional[str] = None      # Researcher/reporter name if externally reported
    fix_available: bool = False
    fix_deployed_at: Optional[datetime] = None
    public_disclosure_at: Optional[datetime] = None
    notification_stage: NotificationStage = NotificationStage.PENDING

    @property
    def requires_enisa_notification(self) -> bool:
        return self.exploit_status in (ExploitStatus.ACTIVE, ExploitStatus.WIDESPREAD)

    @property
    def early_warning_deadline(self) -> datetime:
        return self.discovered_at + timedelta(hours=24)

    @property
    def full_notification_deadline(self) -> datetime:
        return self.discovered_at + timedelta(hours=72)

    @property
    def final_report_deadline(self) -> Optional[datetime]:
        if self.fix_deployed_at:
            return self.fix_deployed_at + timedelta(days=14)
        return None

    @property
    def is_early_warning_overdue(self) -> bool:
        return (
            self.requires_enisa_notification
            and datetime.utcnow() > self.early_warning_deadline
            and self.notification_stage == NotificationStage.PENDING
        )

    @property
    def severity(self) -> str:
        if self.cvss_score >= 9.0: return "CRITICAL"
        if self.cvss_score >= 7.0: return "HIGH"
        if self.cvss_score >= 4.0: return "MEDIUM"
        return "LOW"


@dataclass
class CVDPolicy:
    product_scope: list[str]
    contact_email: str
    pgp_key_url: str
    policy_url: str
    acknowledgment_hours: int = 48       # SLA: acknowledge reporter within N hours
    assessment_days: int = 14            # SLA: triage and severity assessment
    fix_target_days: int = 90            # Standard 90-day disclosure window
    coordinated_disclosure: bool = True

    def to_security_txt(self, domain: str, expires: datetime) -> str:
        lines = [
            f"Contact: mailto:{self.contact_email}",
            f"Expires: {expires.strftime('%Y-%m-%dT%H:%M:%S.000Z')}",
            f"Policy: {self.policy_url}",
            f"Encryption: {self.pgp_key_url}",
            "Preferred-Languages: en",
        ]
        return "\n".join(lines)

    def to_enisa_format(self) -> dict:
        return {
            "scope": self.product_scope,
            "contact": {"email": self.contact_email},
            "sla": {
                "acknowledgment_hours": self.acknowledgment_hours,
                "assessment_days": self.assessment_days,
                "disclosure_days": self.fix_target_days,
            },
            "coordinated_disclosure": self.coordinated_disclosure,
        }


class CRAVulnerabilityReporter:
    def __init__(self, manufacturer_id: str, enisa_api_key: str, cvd_policy: CVDPolicy):
        self.manufacturer_id = manufacturer_id
        self.enisa_api_key = enisa_api_key
        self.cvd_policy = cvd_policy
        self.vulnerabilities: list[CRAVulnerability] = []

    def register_vulnerability(self, vuln: CRAVulnerability) -> str:
        vuln_id = hashlib.sha256(
            f"{vuln.cve_id}{vuln.discovered_at.isoformat()}".encode()
        ).hexdigest()[:12]
        self.vulnerabilities.append(vuln)
        return vuln_id

    def build_early_warning_payload(self, vuln: CRAVulnerability) -> dict:
        return {
            "notification_type": "early_warning",
            "manufacturer_id": self.manufacturer_id,
            "product": {
                "name": vuln.product_name,
                "affected_versions": vuln.product_versions,
            },
            "vulnerability": {
                "id": vuln.cve_id,
                "exploit_status": vuln.exploit_status.value,
                "severity": vuln.severity,
            },
            "discovered_at": vuln.discovered_at.isoformat(),
            "submitted_at": datetime.utcnow().isoformat(),
        }

    def build_full_notification_payload(self, vuln: CRAVulnerability) -> dict:
        return {
            "notification_type": "full_notification",
            "manufacturer_id": self.manufacturer_id,
            "product": {
                "name": vuln.product_name,
                "affected_versions": vuln.product_versions,
            },
            "vulnerability": {
                "id": vuln.cve_id,
                "description": vuln.description,
                "cvss_score": vuln.cvss_score,
                "cvss_vector": vuln.cvss_vector,
                "exploit_status": vuln.exploit_status.value,
                "severity": vuln.severity,
            },
            "mitigation": {
                "fix_available": vuln.fix_available,
                "publicly_disclosed": vuln.public_disclosure_at is not None,
            },
            "discovered_at": vuln.discovered_at.isoformat(),
            "submitted_at": datetime.utcnow().isoformat(),
        }

    def build_final_report_payload(self, vuln: CRAVulnerability) -> dict:
        return {
            "notification_type": "final_report",
            "manufacturer_id": self.manufacturer_id,
            "product": {
                "name": vuln.product_name,
                "affected_versions": vuln.product_versions,
            },
            "vulnerability": {
                "id": vuln.cve_id,
                "description": vuln.description,
                "cvss_score": vuln.cvss_score,
                "cvss_vector": vuln.cvss_vector,
            },
            "remediation": {
                "fix_deployed_at": vuln.fix_deployed_at.isoformat() if vuln.fix_deployed_at else None,
                "public_disclosure_at": vuln.public_disclosure_at.isoformat() if vuln.public_disclosure_at else None,
                "reporter": vuln.reporter,
            },
            "submitted_at": datetime.utcnow().isoformat(),
        }

    def compliance_dashboard(self) -> dict:
        active_vulns = [v for v in self.vulnerabilities if v.requires_enisa_notification]
        overdue_early_warning = [v for v in active_vulns if v.is_early_warning_overdue]
        return {
            "total_vulnerabilities": len(self.vulnerabilities),
            "requiring_enisa_notification": len(active_vulns),
            "overdue_early_warning": len(overdue_early_warning),
            "overdue_ids": [v.cve_id for v in overdue_early_warning],
            "compliance_status": "NON_COMPLIANT" if overdue_early_warning else "COMPLIANT",
        }

CRA × NIS2 Dual-Reporting Overlap

Organisations subject to both CRA and NIS2 face overlapping notification obligations for the same security incident. This is not an accident — the EU deliberately designed these as complementary regimes — but the reporting timelines and recipients differ.

ObligationCRA Art.14NIS2 Art.23
TriggerActively exploited vulnerability in your productSignificant incident affecting your NIS2-covered services
Early warning24 hours to ENISA24 hours to CSIRT/NCA
Full notification72 hours to ENISA72 hours to CSIRT/NCA
Final report14 days post-fix to ENISA1 month post-incident to CSIRT/NCA
RecipientENISA directlyNational CSIRT / NCA
Applies toManufacturers (product makers)Essential and important entities

Key implication: A SaaS manufacturer that is also an important entity under NIS2 must file parallel reports with different recipients. The 24-hour clock starts simultaneously for both obligations. Building a single incident response process that feeds both reporting workflows — ENISA for CRA and national CSIRT for NIS2 — is the practical approach.

The Python CRAVulnerabilityReporter above can be extended with a NIS2IncidentReporter alongside it, sharing the incident metadata but targeting different endpoints and using different payload formats.

Integration with Your SBOM Pipeline (Art.13)

The CRA's notification obligations (Art.14, September 2026) and SBOM requirements (Art.13, December 2027) are designed to work together. An SBOM-first approach makes Art.14 compliance operationally feasible:

  1. SBOM as vulnerability inventory baseline: If you have a machine-readable CycloneDX or SPDX SBOM, vulnerability scanners (Grype, Trivy, OWASP Dependency-Check) can automatically identify when a component vulnerability becomes actively exploited (by cross-referencing CISA KEV or NVD exploit flags)

  2. Automated ENISA notification trigger: When a KEV listing matches a component in your SBOM, the 24-hour clock starts. Automated SBOM scanning + KEV feed polling can detect this and trigger the notification workflow without manual discovery delay

  3. Audit trail integration: SBOM version history (git-tracked CycloneDX files) provides the artefact chain that proves which versions were affected and when the vulnerability was introduced — critical for the final report root cause analysis

Recommended pipeline:

SBOM (CycloneDX) → Daily Grype scan → KEV feed check → CRA notification trigger
                                     ↘ CISA KEV match → 24h early warning
                                     ↘ Full notification (72h)
                                     ↘ Fix deployment → Final report (14d)

September 2026 Readiness: 25-Item Checklist

Governance (Items 1-5)

CVD Policy (Items 6-11)

Notification Process (Items 12-18)

Technical Pipeline (Items 19-23)

Audit Readiness (Items 24-25)

12-Week Implementation Timeline (April → June 2026)

WeekActivityOwner
W1-2Scope review: which products are PDEs? Legal sign-offLegal + Product
W3-4CVD policy drafted, security.txt deployedSecurity lead
W5-6ENISA SRP account registered, notification templates createdSecurity lead
W7-8SBOM pipeline + automated scanner integrated into CI/CDDevOps
W9KEV feed polling + alerting configuredDevOps
W10NIS2 dual-reporting workflow documentedSecurity + Compliance
W11Tabletop exercise: simulated CVE exploitation drillAll teams
W12Gap close, management sign-off, documentation completeManagement

This 12-week timeline delivers June 2026 readiness — three months ahead of the September deadline. The buffer is intentional: NCAs beginning NIS2 audit seasons in June 2026 will look for CRA preparedness as a leading indicator.

Infrastructure Jurisdiction and Notification Scope

One aspect of CRA Art.14 that receives little coverage: the regulation imposes obligations on manufacturers wherever they are established, but the ENISA notification system is designed to route information to national CSIRTs in the member states where affected products are distributed.

If your software is distributed EU-wide (which most SaaS products are), ENISA may route your notification to multiple national CSIRTs simultaneously. This is not a burden to manufacturers — it is the mechanism through which the EU coordinates cross-border vulnerability response. But it means your final report may be reviewed by security authorities in Germany, France, the Netherlands, and Austria concurrently.

From a data residency standpoint: the CRA vulnerability notification data submitted to ENISA is processed by an EU agency operating under EU law. ENISA is headquartered in Athens, with offices in Heraklion. The notification data remains within EU legal jurisdiction — it is not subject to US cloud jurisdiction (CLOUD Act) regardless of where you host your product.

For infrastructure teams concerned about where vulnerability data goes: an ENISA notification submitted from an EU-hosted SaaS (such as sota.io) stays within EU legal jurisdiction end-to-end. The same notification submitted from a US-hosted service is also sent to ENISA (an EU agency), but the cloud infrastructure processing the notification before submission may be subject to CLOUD Act disclosure requests.

Common Compliance Failures to Avoid

Failure 1: "We'll wait for the SRP to launch before building the process" The ENISA SRP is available and accepting test submissions. Building the notification workflow against a "future API" is a planning failure that will compress your implementation window.

Failure 2: "We don't need a CVD policy because we have a security team" Art.15 requires a published CVD policy. Having a security team that handles vulnerability reports internally does not satisfy the publication requirement.

Failure 3: "Our SBOM is in Excel" Excel SBOMs cannot be ingested by automated vulnerability scanners or KEV feed correlation tools. The ENISA reporting pipeline requires programmatic detection — human-readable-only SBOMs create a monitoring gap.

Failure 4: "We submitted the 24-hour early warning — we're done" The three-stage notification is mandatory. Submitting only the early warning and not the full 72-hour notification is partial compliance that NCAs treat as a reporting failure.

Failure 5: "NIS2 already covers this" CRA and NIS2 have different recipients (ENISA vs. national CSIRT) and different triggers (actively exploited product vulnerability vs. significant service incident). They can overlap but are legally separate obligations. NIS2 compliance does not satisfy CRA Art.14.

See Also