EU Cyber Resilience Act: SBOM Requirements and Vulnerability Handling Developer Guide
The EU Cyber Resilience Act (Regulation (EU) 2024/2847, "CRA") entered into force on 10 December 2024. Enforcement begins on 11 December 2027. Any software or hardware product sold in the EU market that has digital elements — this includes SaaS platforms, on-premises software, embedded systems, mobile apps, and cloud-hosted services accessed via network — must comply.
The CRA introduces four categories of mandatory developer obligations that have no counterpart in prior EU regulation:
- Software Bill of Materials (Art. 13): machine-readable inventory of all components
- Coordinated Vulnerability Disclosure (Art. 14): 24-hour ENISA notification for actively exploited vulnerabilities
- Security Update Delivery (Art. 15): mandatory updates for at least 5 years
- Security-by-Design (Art. 11): secure defaults, minimal attack surface, least privilege
This guide covers each article in implementation detail, explains the critical intersections with the EU AI Act and NIS2 Directive, and identifies where infrastructure jurisdiction determines your compliance posture.
Scope: What Is a "Product with Digital Elements"?
The CRA applies to any product that has data processing functionality and is placed on the EU market — whether sold, licensed, or provided via subscription. Recital 12 and Art. 3(1) define the scope broadly:
- Software products: desktop applications, server software, operating systems, firmware, mobile apps
- SaaS and cloud-hosted services: if the software component is the primary product (not ancillary processing). A cloud storage service is a PDE; a bank's internal IT system is not
- Hardware with embedded software: routers, IoT devices, industrial controllers, medical devices
- Open source software: only if commercialised (sold, bundled with paid services, or used as basis for revenue-generating products). Pure volunteer open source with no commercial use is exempt (Art. 16)
Class A (Standard PDEs): Default CRA obligations. Self-assessment of conformity possible.
Class B (Important PDEs — Annex III): Higher-risk categories including browsers, password managers, VPN clients, network management software, SIEM systems, and industrial automation software. Third-party conformity assessment required for critical categories.
The classification determines not which obligations apply — all PDEs face Art. 11/13/14/15 — but how conformity is assessed.
Art. 11: Security-by-Design Obligations
Article 11 requires that products with digital elements are designed and developed to deliver security as a default property, not as a post-deployment addition. The operative requirements:
11(1) — No Known Exploitable Vulnerabilities at Placement
Products must be placed on the market without known exploitable vulnerabilities in components. This does not require zero CVEs — it requires that all known CVEs are either patched, mitigated, or explicitly documented with compensating controls.
Practical implication: you cannot ship a container image with log4j 2.14 and CVE-2021-44228 unpatched. Market surveillance authorities (MSAs) can pull products from sale if placed with known critical vulnerabilities unaddressed.
11(2) — Secure Default Configuration
Default configurations must implement least-privilege. Specific prohibitions:
- No insecure default credentials: factory-default passwords like
admin/admin,root/root, or device-serial-number-based passwords are prohibited. Each instance must have unique initial credentials, or setup must force credential creation before first use - No unnecessary attack surface: services, ports, and protocols not required for core functionality must be disabled by default. SSH on port 22 enabled by default on a consumer IoT device = non-compliant
- Automatic security updates enabled by default (with user opt-out permitted — see Art. 15)
11(3) — Data Protection at Design Level
Products must implement:
- Encryption in transit and at rest for personal data and security-relevant data (keys, certificates, credentials)
- Data minimisation: products must not collect data beyond operational necessity
- Access control: authentication mechanisms for all administrative interfaces
11(4) — Breach Notification Capability
Products must include mechanisms to detect and report security incidents. For software: structured logging of authentication failures, configuration changes, and anomalous access patterns. For embedded devices: tamper detection.
# Art.11(4)-compliant security event logging
import hashlib
import json
import time
from datetime import datetime, timezone
class SecurityEventLog:
"""Tamper-evident security event log for CRA Art.11(4) compliance."""
def __init__(self, log_path: str):
self.log_path = log_path
self._prev_hash = "genesis"
def record(self, event_type: str, details: dict, severity: str = "INFO") -> str:
"""Record security event with chain hash for tamper detection."""
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": event_type,
"severity": severity,
"details": details,
"prev_hash": self._prev_hash,
}
entry_json = json.dumps(entry, sort_keys=True)
entry_hash = hashlib.sha256(entry_json.encode()).hexdigest()
entry["hash"] = entry_hash
self._prev_hash = entry_hash
with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry_hash
def record_auth_failure(self, user_id: str, source_ip: str, method: str):
return self.record(
"AUTH_FAILURE",
{"user_id": user_id, "source_ip": source_ip, "method": method},
severity="WARNING"
)
def record_config_change(self, changed_by: str, parameter: str, old_val, new_val):
return self.record(
"CONFIG_CHANGE",
{"changed_by": changed_by, "parameter": parameter,
"old_value": str(old_val), "new_value": str(new_val)},
severity="AUDIT"
)
# Usage
log = SecurityEventLog("/var/log/security-events.jsonl")
log.record_auth_failure("user@example.com", "203.0.113.42", "password")
Art. 13: Software Bill of Materials (SBOM) Requirements
Article 13(1)(b) and Annex I Part II require that manufacturers of products with digital elements maintain a machine-readable Software Bill of Materials covering all third-party components and dependencies.
Mandatory SBOM Content
The CRA does not prescribe every field, but Annex I Part II and ENISA guidance specify minimum content per component:
| Field | Description | Example |
|---|---|---|
| Component name | Library or package name | fastapi |
| Version | Exact version string | 0.115.6 |
| Supplier | Author/maintainer | Sebastián Ramírez |
| Unique identifier | CPE 2.3 or Package URL (PURL) | pkg:pypi/fastapi@0.115.6 |
| Hash | SHA-256 of distributed artifact | sha256:abc123... |
| License | SPDX license expression | MIT |
| Relationship | Relationship to parent component | DEPENDENCY_OF |
Transitive dependencies (dependencies of dependencies) must be included. For a typical Python service with 30 direct dependencies, the full transitive closure typically covers 150-300 packages.
SBOM Formats: CycloneDX vs SPDX
The CRA does not mandate a specific format, but requires machine-readability. Two formats dominate:
CycloneDX (recommended for security workflows):
- OWASP-maintained. JSON and XML serialisations
- Native vulnerability enrichment via VEX (Vulnerability Exploitability eXchange)
- Supports hardware BOM, cryptography BOM, machine learning BOM
- Tooling:
cyclonedx-bom(Python),cyclonedx-gomod(Go),syft,cdxgen
SPDX (recommended for license compliance):
- Linux Foundation / ISO/IEC 5962:2021 standard
- Strong license expression language
- Tooling:
spdx-tools,syft --output spdx-json,trivy --format spdx
# Generate CycloneDX SBOM from Python project
# pip install cyclonedx-bom
import subprocess
import json
from pathlib import Path
def generate_sbom(project_root: Path, output_path: Path, format: str = "json") -> dict:
"""Generate CRA Art.13-compliant SBOM using cyclonedx-bom."""
result = subprocess.run(
["cyclonedx-py", "environment", "--output-format", format,
"--output-file", str(output_path)],
cwd=project_root,
capture_output=True,
text=True
)
if result.returncode != 0:
raise RuntimeError(f"SBOM generation failed: {result.stderr}")
with open(output_path) as f:
sbom = json.load(f)
# Validate required fields per CRA Annex I Part II
components = sbom.get("components", [])
missing_fields = []
for comp in components:
if not comp.get("name"):
missing_fields.append(f"{comp}: missing name")
if not comp.get("version"):
missing_fields.append(f"{comp.get('name')}: missing version")
if not comp.get("purl") and not any(
ref.get("type") == "cpe23Type" for ref in comp.get("externalReferences", [])
):
missing_fields.append(f"{comp.get('name')}: missing PURL/CPE identifier")
return {"sbom": sbom, "component_count": len(components), "warnings": missing_fields}
SBOM Storage and Disclosure Requirements
The CRA does not require public SBOM publication. Manufacturers must:
- Maintain the SBOM for the product's supported lifetime
- Provide it to market surveillance authorities on request
- Make it available to professional users on request (B2B supply chains)
Critical infrastructure consideration: If your SBOM is stored on US cloud infrastructure (AWS S3, Azure Blob, GCP Storage), it is subject to the US CLOUD Act (18 U.S.C. § 2713). US Department of Justice can compel disclosure of SBOM contents to US law enforcement without EU court involvement — including component lists, version details, and vulnerability information about your product.
For products in CRA Class B (Annex III), market surveillance authorities may share SBOM contents across EU member states via the ENISA vulnerability database. Storing SBOMs on EU-native infrastructure keeps this data under EU administrative law exclusively.
Art. 14: Coordinated Vulnerability Disclosure
Article 14 introduces the most operationally demanding CRA obligation for most development teams: mandatory vulnerability disclosure with hard time limits.
The Three-Stage Disclosure Timeline
When a manufacturer becomes aware of an actively exploited vulnerability in their product:
Stage 1 — Early Warning (24 hours): Notify ENISA via the single reporting platform with: product name, vulnerability type, CVE/identifier if known, affected versions, potential impact, whether a fix is available.
Stage 2 — Vulnerability Notification (72 hours): Submit detailed vulnerability report including: root cause analysis, affected component details, CVSS score, remediation steps, workaround instructions if patch not yet available.
Stage 3 — Final Report (14 days after remediation): Submit final report documenting: patch details, coordinated disclosure timeline, affected user population estimate, mitigation adoption status.
The 24-hour trigger is "actively exploited" — meaning confirmed reports of exploitation in the wild, not mere discovery of a vulnerability. Vulnerabilities discovered internally that are not yet exploited trigger coordinated disclosure obligations (Art. 14(3)) but not the 24-hour emergency timeline.
# CRA Art.14-compliant vulnerability disclosure workflow
import httpx
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta
from enum import Enum
class DisclosureStage(Enum):
EARLY_WARNING = "early_warning" # 24h deadline
VULNERABILITY_REPORT = "vuln_report" # 72h deadline
FINAL_REPORT = "final_report" # 14 days deadline
@dataclass
class VulnerabilityDisclosure:
cve_id: str
product_name: str
affected_versions: list[str]
severity_cvss: float
exploited_in_wild: bool
discovered_at: datetime
patch_available: bool = False
patch_version: str | None = None
def get_deadline(self, stage: DisclosureStage) -> datetime:
deadlines = {
DisclosureStage.EARLY_WARNING: timedelta(hours=24),
DisclosureStage.VULNERABILITY_REPORT: timedelta(hours=72),
DisclosureStage.FINAL_REPORT: timedelta(days=14),
}
return self.discovered_at + deadlines[stage]
def is_overdue(self, stage: DisclosureStage) -> bool:
return datetime.now(timezone.utc) > self.get_deadline(stage)
def hours_remaining(self, stage: DisclosureStage) -> float:
delta = self.get_deadline(stage) - datetime.now(timezone.utc)
return delta.total_seconds() / 3600
def submit_enisa_early_warning(disclosure: VulnerabilityDisclosure, enisa_api_token: str):
"""Submit CRA Art.14 Stage 1 early warning to ENISA platform."""
payload = {
"product_name": disclosure.product_name,
"vulnerability_id": disclosure.cve_id,
"affected_versions": disclosure.affected_versions,
"actively_exploited": disclosure.exploited_in_wild,
"fix_available": disclosure.patch_available,
"severity": "CRITICAL" if disclosure.severity_cvss >= 9.0 else
"HIGH" if disclosure.severity_cvss >= 7.0 else "MEDIUM",
"reported_at": disclosure.discovered_at.isoformat(),
}
# ENISA single reporting platform (operational from 2027)
response = httpx.post(
"https://euvd.enisa.europa.eu/api/v1/disclosures/early-warning",
json=payload,
headers={"Authorization": f"Bearer {enisa_api_token}"},
timeout=30,
)
response.raise_for_status()
return response.json()
Actively Exploited vs Discovered: Threshold Clarity
The 24-hour deadline applies only to actively exploited vulnerabilities. For vulnerabilities discovered internally:
| Discovery Scenario | CRA Obligation | Timeline |
|---|---|---|
| Internal code review finds a bug | Coordinated disclosure + patch | No hard timer until exploitation confirmed |
| Bug bounty report (unexploited) | Acknowledge reporter, begin CVD process | Reasonable disclosure timeline (typically 90 days per industry standard) |
| External report of active exploitation | Art. 14 mandatory ENISA notification | 24h early warning |
| ENISA-requested disclosure | Mandatory cooperation | Per ENISA request terms |
NIS2 intersection: NIS2 Art. 23 requires essential entities to notify their national CSIRT within 24 hours of becoming aware of a "significant incident." A CRA-reportable actively exploited vulnerability in a product used by essential entities may simultaneously trigger CRA Art. 14 ENISA notification AND NIS2 Art. 23 CSIRT notification. These are separate reporting obligations to different authorities.
Art. 15: Security Update Delivery
Article 15 requires manufacturers to:
- Deliver security updates for the product's support period
- Minimum support period: the shorter of (a) the expected lifetime of the product in its context of use, or (b) 5 years from market placement
- Separate security updates from functional updates: security patches must be deliverable independently, without requiring users to also accept new features
- Automatic update option: products must offer automatic security update delivery. Users may opt out, but the option must exist and be enabled by default (Art. 11(2)(f))
- User notification: users must be notified of available updates, their urgency level, and actions required
# CRA Art.15-compliant update management
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
class UpdateType(Enum):
SECURITY = "security" # Must be separable from functional updates
FUNCTIONAL = "functional"
COMBINED = "combined" # Allowed only if functional changes required for security fix
class UpdateUrgency(Enum):
CRITICAL = "critical" # Exploit available, deploy immediately
HIGH = "high" # No exploit yet, 7-day deployment window
MEDIUM = "medium" # 30-day deployment window
LOW = "low" # 90-day deployment window
@dataclass
class SecurityUpdate:
version: str
update_type: UpdateType
urgency: UpdateUrgency
cve_ids: list[str]
cvss_scores: list[float]
release_date: datetime
end_of_support_date: datetime
auto_apply_eligible: bool = True
def days_until_eol(self) -> int:
delta = self.end_of_support_date - datetime.now()
return delta.days
def recommended_deployment_deadline(self) -> datetime:
windows = {
UpdateUrgency.CRITICAL: timedelta(days=1),
UpdateUrgency.HIGH: timedelta(days=7),
UpdateUrgency.MEDIUM: timedelta(days=30),
UpdateUrgency.LOW: timedelta(days=90),
}
return self.release_date + windows[self.urgency]
class UpdatePolicyManager:
"""CRA Art.15-compliant update policy for products with digital elements."""
def __init__(self, product_name: str, market_placement_date: datetime,
expected_lifetime_years: int = 5):
self.product_name = product_name
# Art.15: support period = min(expected lifetime, 5 years)
self.support_end = market_placement_date + timedelta(
days=365 * min(expected_lifetime_years, 5)
)
def is_in_support(self) -> bool:
return datetime.now() < self.support_end
def generate_update_manifest(self, update: SecurityUpdate) -> dict:
"""Generate machine-readable update manifest for automatic update systems."""
return {
"product": self.product_name,
"update_version": update.version,
"update_type": update.update_type.value,
"urgency": update.urgency.value,
"cve_ids": update.cve_ids,
"max_cvss": max(update.cvss_scores, default=0),
"release_date": update.release_date.isoformat(),
"deployment_deadline": update.recommended_deployment_deadline().isoformat(),
"auto_apply_eligible": update.auto_apply_eligible,
"in_support_period": self.is_in_support(),
"support_ends": self.support_end.isoformat(),
}
CRA × EU AI Act Intersection
High-risk AI systems under EU AI Act Annex III are almost universally also Products with Digital Elements under the CRA. This creates dual compliance obligations that partially overlap but are independently enforced.
Where Obligations Overlap
| Obligation Area | AI Act Requirement | CRA Requirement |
|---|---|---|
| Documentation | Art. 11 Technical Documentation | Art. 13 SBOM |
| Risk Management | Art. 9 Risk Management System | Art. 11 Secure-by-Design |
| Post-Market Monitoring | Art. 72 Post-Market Monitoring | Art. 14 Vulnerability Disclosure |
| Incident Reporting | Art. 73 Serious Incident Reporting | Art. 14 ENISA Notification |
| Updates | Art. 72(4) Corrective Actions | Art. 15 Security Updates |
Where Obligations Diverge
AI Act Art. 72 (Post-Market Monitoring) covers AI-specific incidents — unintended outputs, performance degradation, discriminatory outcomes, safety failures. CRA Art. 14 covers security vulnerabilities — exploitable weaknesses that attackers can leverage.
These are different incident categories that may require simultaneous reporting to different authorities:
- AI Act Art. 73 serious incident → national market surveillance authority (and EU AI Office for general-purpose AI models)
- CRA Art. 14 actively exploited vulnerability → ENISA
A compromised high-risk AI system (e.g., an attacker manipulates model inputs to cause discriminatory outputs) may simultaneously trigger both reporting obligations.
AI Act Art. 9 FMEA requirement: Risk management must include foreseeable misuse scenarios. A CRA Art. 14 vulnerability in the AI system's inference API that enables adversarial examples is a foreseeable misuse scenario that should appear in the Art. 9 risk register — bridging the two frameworks.
# Unified CRA × AI Act incident classifier
from dataclasses import dataclass
from enum import Enum
class ReportingAuthority(Enum):
ENISA = "ENISA" # CRA Art.14
NATIONAL_MSA = "National_MSA" # AI Act Art.73
BOTH = "Both_ENISA_and_MSA"
@dataclass
class SecurityIncident:
affects_ai_system: bool
ai_system_is_high_risk: bool # AI Act Annex III
is_actively_exploited: bool
causes_ai_output_harm: bool
personal_data_breach: bool # GDPR Art.33 also triggered
def required_reporting(self) -> list[ReportingAuthority]:
authorities = []
if self.is_actively_exploited:
authorities.append(ReportingAuthority.ENISA) # CRA Art.14, 24h
if self.affects_ai_system and self.ai_system_is_high_risk and self.causes_ai_output_harm:
authorities.append(ReportingAuthority.NATIONAL_MSA) # AI Act Art.73
return authorities if authorities else [ReportingAuthority.NATIONAL_MSA]
def enisa_deadline_hours(self) -> int | None:
return 24 if self.is_actively_exploited else None
def gdpr_deadline_hours(self) -> int | None:
return 72 if self.personal_data_breach else None
CRA × NIS2 Intersection
The CRA and NIS2 Directive address cybersecurity from different angles:
- CRA: product compliance — applies to the software/hardware manufacturer
- NIS2: operational compliance — applies to entities operating essential or important services
The same organisation can be subject to both simultaneously: if you manufacture cybersecurity software (CRA applies to the product) and you operate a digital infrastructure service (NIS2 applies to your operations).
Where the Rules Diverge
| Dimension | CRA | NIS2 |
|---|---|---|
| Subject | Product manufacturer | Essential/Important entity |
| Scope trigger | Placing a PDE on the EU market | Operating in a covered sector |
| Vulnerability reporting | ENISA (24h for active exploitation) | National CSIRT (24h for significant incidents) |
| Enforcement | Market surveillance authority | National competent authority |
| Fines | Up to €15M or 2.5% global turnover | Up to €10M (important) / €20M (essential) or 1.7%/2% |
The dual-reporting trap: A CRA-reportable actively exploited vulnerability in your product is not automatically a NIS2-reportable significant incident for your operations — the thresholds differ. A significant incident under NIS2 Art. 23 requires measurable operational impact (service disruption, data breach, loss of integrity). A vulnerability without confirmed operational impact may not reach that threshold.
However, if the actively exploited vulnerability does cause operational disruption — particularly for NIS2-essential services — you face simultaneous:
- CRA Art. 14 → ENISA notification (24h)
- NIS2 Art. 23 → National CSIRT notification (24h)
- GDPR Art. 33 → National DPA notification (72h, if personal data involved)
Infrastructure Jurisdiction and CRA Compliance
The CRA creates three specific scenarios where infrastructure jurisdiction affects compliance posture:
1. SBOM Data Sovereignty
Art. 13 SBOMs stored on US cloud infrastructure (AWS S3, Azure Blob, GCP Storage) are subject to the US CLOUD Act. The DoJ can compel AWS/Microsoft/Google to disclose SBOM contents without EU court involvement. For competitive or national security-sensitive products, this creates a structural exposure:
- Your component list reveals your entire supply chain
- Vulnerabilities in your components become known to US authorities before you've disclosed to ENISA
- For CRA Class B products (Annex III), market surveillance authorities share data with ENISA's vulnerability database — if your SBOM reaches ENISA via a parallel CLOUD Act channel, you may face disclosure compliance questions before filing your Art. 14 report
EU-native SBOM storage resolves this: data remains subject exclusively to EU administrative law. Market surveillance authorities can request it through established EU legal channels; no parallel CLOUD Act route exists.
2. Vulnerability Disclosure Infrastructure
Art. 14 early warning submissions to ENISA should originate from infrastructure your team controls entirely. If your incident response tooling — SIEMs, vulnerability scanners, disclosure workflow systems — runs on US cloud, the following scenarios arise:
- US law enforcement may access your vulnerability intelligence before ENISA does
- If a vulnerability involves a US government customer or US national security system, CLOUD Act orders can delay or suppress your Art. 14 disclosure
- For CRA Class B products, market surveillance authorities in different member states can access your ENISA submissions — US cloud operators cannot guarantee this data doesn't also flow through US government access channels
3. Security Update Delivery
Art. 15 requires delivering security updates for 5+ years. If your update delivery infrastructure — package repositories, CDNs, OTA update endpoints — runs on US cloud:
- CLOUD Act orders can compel the cloud provider to intercept or delay update delivery to specific customers
- For products used in EU critical infrastructure, NIS2 supervisory authorities may require assurance that update channels are not subject to third-party (non-EU) access
EU-native update delivery infrastructure removes these concerns structurally.
CRA Compliance Checklist
# CRA compliance readiness assessment
from dataclasses import dataclass, field
@dataclass
class CRAComplianceStatus:
product_name: str
# Art. 11 — Security-by-Design
no_default_shared_passwords: bool = False
automatic_updates_default_on: bool = False
minimal_attack_surface_documented: bool = False
security_event_logging_implemented: bool = False
# Art. 13 — SBOM
sbom_generated: bool = False
sbom_format_cyclonedx_or_spdx: bool = False
sbom_includes_transitive_deps: bool = False
sbom_stored_eu_jurisdiction: bool = False
sbom_updated_on_every_release: bool = False
# Art. 14 — Vulnerability Disclosure
cvd_policy_published: bool = False
enisa_reporting_workflow_documented: bool = False
incident_response_24h_capable: bool = False
vuln_disclosure_contact_public: bool = False
# Art. 15 — Security Updates
support_period_minimum_5_years: bool = False
security_updates_separable_from_functional: bool = False
auto_update_mechanism_implemented: bool = False
eol_date_published: bool = False
def score(self) -> tuple[int, int]:
fields = [
self.no_default_shared_passwords,
self.automatic_updates_default_on,
self.minimal_attack_surface_documented,
self.security_event_logging_implemented,
self.sbom_generated,
self.sbom_format_cyclonedx_or_spdx,
self.sbom_includes_transitive_deps,
self.sbom_stored_eu_jurisdiction,
self.sbom_updated_on_every_release,
self.cvd_policy_published,
self.enisa_reporting_workflow_documented,
self.incident_response_24h_capable,
self.vuln_disclosure_contact_public,
self.support_period_minimum_5_years,
self.security_updates_separable_from_functional,
self.auto_update_mechanism_implemented,
self.eol_date_published,
]
passed = sum(1 for f in fields if f)
return passed, len(fields)
def gaps(self) -> list[str]:
checks = {
"Art.11 — No default shared passwords": self.no_default_shared_passwords,
"Art.11 — Automatic updates enabled by default": self.automatic_updates_default_on,
"Art.11 — Attack surface documented": self.minimal_attack_surface_documented,
"Art.11 — Security event logging": self.security_event_logging_implemented,
"Art.13 — SBOM generated": self.sbom_generated,
"Art.13 — CycloneDX or SPDX format": self.sbom_format_cyclonedx_or_spdx,
"Art.13 — Transitive dependencies included": self.sbom_includes_transitive_deps,
"Art.13 — SBOM stored in EU jurisdiction": self.sbom_stored_eu_jurisdiction,
"Art.13 — SBOM updated per release": self.sbom_updated_on_every_release,
"Art.14 — CVD policy published": self.cvd_policy_published,
"Art.14 — ENISA reporting workflow documented": self.enisa_reporting_workflow_documented,
"Art.14 — 24h incident response capable": self.incident_response_24h_capable,
"Art.14 — Vulnerability disclosure contact public": self.vuln_disclosure_contact_public,
"Art.15 — 5-year minimum support period": self.support_period_minimum_5_years,
"Art.15 — Security updates separable": self.security_updates_separable_from_functional,
"Art.15 — Auto-update mechanism": self.auto_update_mechanism_implemented,
"Art.15 — EOL date published": self.eol_date_published,
}
return [label for label, passed in checks.items() if not passed]
Open Source and the CRA
The CRA's treatment of open source software has been controversial. The final text (Art. 16) exempts software that is:
- Developed entirely outside of a commercial activity
- Not intended for commercial use by the developer
Open source components integrated into commercial products are not exempt — the commercial product manufacturer bears CRA obligations for those components. A startup building on an open source framework must maintain an SBOM that includes the framework, notify ENISA if a framework vulnerability is actively exploited in their product, and deliver security updates that address framework vulnerabilities.
The practical burden: open source ecosystem vulnerabilities (like Log4Shell, XZ Utils, or OpenSSL heartbleed) are now the manufacturer's responsibility to track, SBOM-document, and report if actively exploited in their product. This makes continuous dependency scanning (Dependabot, Renovate, OWASP Dependency-Check) a regulatory requirement, not just a good practice.
Implementation Timeline
| Date | CRA Milestone |
|---|---|
| 10 December 2024 | CRA enters into force |
| 11 September 2026 | Art. 14 vulnerability disclosure obligations apply |
| 11 December 2027 | Full CRA enforcement — all obligations active |
| Ongoing from 2027 | 5-year minimum security update support periods begin |
The September 2026 vulnerability disclosure deadline arrives before full enforcement — development teams should implement Art. 14 workflows and ENISA reporting capabilities before their 2027 product compliance deadline.
Conclusion
The EU Cyber Resilience Act creates four independent but interconnected developer obligations: security-by-design (Art. 11), machine-readable component transparency (Art. 13), coordinated vulnerability disclosure with hard timelines (Art. 14), and multi-year security update commitments (Art. 15). For teams building high-risk AI systems, these obligations layer on top of EU AI Act Art. 9/11/72/73 requirements — creating parallel documentation, monitoring, and incident reporting workflows that must be designed to coexist.
Infrastructure jurisdiction is not a peripheral concern under the CRA. SBOM storage, vulnerability disclosure tooling, and update delivery infrastructure all have data governance implications that differ materially between EU-native and US-cloud deployments. Teams that resolve this at the infrastructure layer — choosing EU-native hosting for compliance-critical data and update delivery — eliminate an entire category of regulatory exposure before the first line of CRA-compliance code is written.
See Also
- CRA Art.9: Due Diligence for Third-Party Components — SBOM obligations, open source due diligence, and supply chain vulnerability monitoring
- CRA Art.10: Security Obligations During the Product Lifecycle — Ongoing post-market security update and patch obligations
- CRA Art.11: Vulnerability Handling and Responsible Disclosure — CVD policy, ban on shipping known exploitable bugs, SBOM tracking