TL;DR The EU Cyber Resilience Act (CRA) is not a future problem anymore: Article 14 vulnerability-reporting obligations apply from 11 September 2026, and the full Annex I requirements - including a machine-readable SBOM as part of your technical documentation - apply from 11 December 2027. Reporting a vulnerability within 24 hours only works if you already know which components, and which versions, are in the product. For a TwinCAT machine that means every Twinpack package and every Beckhoff/CODESYS system library the PLC links against - not just the application code you wrote yourself.
zkmake sbomgenerates that inventory as a CycloneDX 1.6 document on every build, on-premises or viazkbuild-action, so it exists before anyone asks for it.
The deadline that already passed - or is about to
Talk to most TwinCAT shops about the Cyber Resilience Act and you get one of two reactions: “that’s 2027, we have time,” or “does this even apply to us?” Both miss what already happened.
The CRA entered into force on 10 December 2024. It phases in over three years, and the first operative date is not December 2027:
- 11 September 2026 - manufacturers must report actively exploited vulnerabilities to ENISA and the relevant national CSIRT within 24 hours of becoming aware of them (Article 14). This applies now, to products already on the market.
- 11 December 2027 - the full set of Annex I essential requirements applies: secure-by-design defaults, a defined support period, coordinated vulnerability disclosure, and technical documentation that includes a machine-readable SBOM. No CE marking without it.
Whether a specific machine falls in scope - “product with digital elements,” network connection, commercial availability - is a legal question for your compliance team, not something a blog post can settle for you. But if your machine talks to a network and you sell more than one of it, you should assume the question is “when,” not “if.”
Why the 2026 date matters even though the SBOM mandate is 2027
The two dates are not independent. You cannot report which product versions and components are affected by a reported vulnerability within 24 hours if nobody in the building can currently answer “which version of which library did we ship in the machines running out there.” That answer is exactly what an SBOM is.
So in practice, the accepted reading across CRA compliance guidance is: the reporting duty starts in 2026, and having accurate, current component visibility is a practical precondition for meeting it - even though the formal SBOM requirement in Annex VII of the technical documentation is a 2027 obligation. Waiting for 2027 to start building the SBOM means you spend 2026 unable to answer a regulator’s question fast enough.
What this looks like for a TwinCAT project specifically
TwinCAT gives you no built-in answer to “what’s actually in this PLC application.” A .plcproj references packages by name and version, but:
- Beckhoff system libraries (
Tc2_Standard,Tc3_EventLogger, …) are resolved from whatever is installed in the local TwinCAT repository, not stated as a dependency anywhere in source control. - CODESYS-authored libraries that TwinCAT bundles (
SysMem,Base Interfaces, …) often state a category like “System” as their company in the archive metadata, not a vendor - so a naive reader either drops the vendor or invents one. - HMI projects pull in TE2000 NuGet packages (
Beckhoff.TwinCAT.HMI.Framework,.Controls,.EventLogger, …) throughpackages.config, a completely separate mechanism from how the PLC side resolves its references. - Versions are frequently unpinned (
*), which is fine for development and a genuine gap for a document that is supposed to state what was actually delivered.
None of this is exotic - it is just what “no built-in CI/CD” (see our build tools landscape post) costs you the moment someone outside engineering asks for a component inventory instead of a working machine.
What zkmake sbom actually produces
zkmake sbom walks the same information TwinCAT itself has - the resolved Twinpack packages, the local library repository, the boot project’s linked references, and (since our latest release) the HMI’s packages.config and its published TE2000 packages - and emits one CycloneDX 1.6 document per build:
Every delivered PLC library and application, every resolved Twinpack package, every TwinCAT/Beckhoff/CODESYS system library, and every HMI package, each with name, exact version, license, and a SHA-256 (or SHA-512) hash of the artifact that was actually built.
License attribution that follows provenance where the archive itself states none: a library that ships as part of the TwinCAT installation is attributed to the Beckhoff EULA rather than left blank, while a genuine third-party library dropped into the same repository is not - a blank license is reported, not guessed away.
A completeness flag (
bsi:license-completeness,bsi:version-completeness) so a build that has an unresolved license or an unpinned version says so explicitly in the document, instead of silently shipping a gap.Traceability back to the commit, branch, and build that produced it, via the same
references.jsonyour pipeline already writes.Cloud: part of every
zkbuild-actionrun, no separate step.On-premises:
zkmake sbom, one CLI call afterzkmake build, part of the licensedzkmakeCLI.
This is not a compliance product - it will not tell you whether your machine is in scope for the CRA, and it does not do vulnerability scanning by itself. It is the input a vulnerability scanner, a customer security questionnaire, or your own compliance process needs, generated automatically instead of maintained by hand in a spreadsheet that is out of date by the second build.
What a complete document looks like
Below is the CycloneDX 1.6 SBOM for the Zeugwerk Framework Quickstart, written by zkmake sbom --require-artifacts. The command is documented in the zkmake documentation. The project is PLC-only, so the document has no HMI packages. Every component has a resolved version, a license, and a hash of the file that was actually delivered.
- The application is licensed under the Zeugwerk End User License Agreement. Set that with
--license, or with thelicensefield on the PLC in.Zeugwerk/config.json. If a first-party PLC states no license,zkmakeemitsNOASSERTIONandbsi:license-completenessstaysincomplete. - The Framework libraries
ZCore,ZAux,ZApplication,ZEquipment, andZPlatformalready state ZF1000 (OEM License) in their Twinpack metadata. That is the Zeugwerk EULA for those packages. Tc3_Moduleis the Beckhoff system library. The project reference is*. The SBOM records the resolved version and the Beckhoff End User License Agreement.
bsi:license-completeness and bsi:version-completeness are both complete.
Download sbom-Zeugwerk_Quickstart-1.9.0.1-TC3.1.cdx.json
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:d1037499-46e3-45f3-a00f-10aaa9fba83b",
"version": 1,
"metadata": {
"timestamp": "2026-10-14T16:19:05Z",
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://www.zeugwerk.at"
]
},
"tools": {
"components": [
{
"type": "application",
"name": "zkmake",
"version": "1.13.36.0",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://www.zeugwerk.at"
]
}
}
]
},
"component": {
"type": "application",
"bom-ref": "Zeugwerk_Quickstart@1.9.0.1",
"name": "Zeugwerk_Quickstart",
"version": "1.9.0.1",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://www.zeugwerk.at"
]
},
"licenses": [
{
"license": {
"name": "Zeugwerk End User License Agreement"
}
}
]
},
"properties": [
{
"name": "bsi:license-completeness",
"value": "complete"
},
{
"name": "bsi:version-completeness",
"value": "complete"
},
{
"name": "tcversion",
"value": "TC3.1"
},
{
"name": "compiled",
"value": "false"
}
]
},
"components": [
{
"type": "library",
"bom-ref": "Beckhoff Automation GmbH/Tc3_Module@3.4.7.0",
"name": "Tc3_Module",
"version": "3.4.7.0",
"scope": "required",
"supplier": {
"name": "Beckhoff Automation GmbH"
},
"manufacturer": {
"name": "Beckhoff Automation GmbH"
},
"authors": [
{
"name": "Beckhoff Automation GmbH"
}
],
"licenses": [
{
"license": {
"name": "Beckhoff End User License Agreement"
}
}
],
"purl": "pkg:generic/Beckhoff%20Automation%20GmbH/Tc3_Module@3.4.7.0",
"hashes": [
{
"alg": "SHA-512",
"content": "c9a0a84a832865fb26fc7c67d0532300e10465d6a5f21bd3b7dde0742839597d543592bc953aacf19a1e5d205f1d05e94385e531de557c7ab550012643077307"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "filename",
"value": "TwinCAT.XAE.PLC.Lib.Tc3_Module_3.4.7.0.compiled-library-ge33"
},
{
"name": "archive",
"value": "archive"
},
{
"name": "twincat:repository",
"value": "C:\\ProgramData\\Beckhoff\\TwinCAT\\PlcEngineering\\Managed Libraries"
},
{
"name": "twincat:repository-folder",
"value": "Beckhoff Automation GmbH"
},
{
"name": "twincat:version-resolution",
"value": "highest-installed"
}
]
},
{
"type": "library",
"bom-ref": "Zeugwerk GmbH/ZApplication@1.9.2.0",
"name": "ZApplication",
"version": "1.9.2.0",
"description": "The Zeugwerk Application Library serves as the cornerstone of the Zeugwerk Framework, providing a cohesive and structured foundation for building a wide range of industrial applications that we call \u0022Zeugwerk Application Pattern\u0022.",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://doc.zeugwerk.dev"
]
},
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"licenses": [
{
"license": {
"name": "ZF1000 (OEM License)"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/ZApplication@1.9.2.0?branch=main\u0026target=TC3.1\u0026configuration=Debug",
"hashes": [
{
"alg": "SHA-256",
"content": "b4007c935e082e610c568a6f959cd0322263547d7901d46f6aac1cc6e1110350"
}
],
"externalReferences": [
{
"type": "website",
"url": "https://doc.zeugwerk.dev"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "twinpack:branch",
"value": "main"
},
{
"name": "twinpack:target",
"value": "TC3.1"
},
{
"name": "twinpack:configuration",
"value": "Debug"
}
]
},
{
"type": "library",
"bom-ref": "Zeugwerk GmbH/ZAux@1.9.2.0",
"name": "ZAux",
"version": "1.9.2.0",
"description": "The Zeugwerk Aux Library is a dynamic and versatile addition to the Zeugwerk Framework, designed to facilitate seamless sequential flow control, advanced logging capabilities, and effortless component instantiation.",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://doc.zeugwerk.dev"
]
},
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"licenses": [
{
"license": {
"name": "ZF1000 (OEM License)"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/ZAux@1.9.2.0?branch=main\u0026target=TC3.1\u0026configuration=Debug",
"hashes": [
{
"alg": "SHA-256",
"content": "fa4d335c8aee8e8b690b35cdd49b783802448f9afa6fea9701e1845d8ac66907"
}
],
"externalReferences": [
{
"type": "website",
"url": "https://doc.zeugwerk.dev"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "twinpack:branch",
"value": "main"
},
{
"name": "twinpack:target",
"value": "TC3.1"
},
{
"name": "twinpack:configuration",
"value": "Debug"
}
]
},
{
"type": "library",
"bom-ref": "Zeugwerk GmbH/ZCore@1.9.2.0",
"name": "ZCore",
"version": "1.9.2.0",
"description": "The Zeugwerk Framework\u0027s Core Library is a crucial component designed to provide a robust and versatile foundation for building industrial-grade applications.",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://doc.zeugwerk.dev"
]
},
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"licenses": [
{
"license": {
"name": "ZF1000 (OEM License)"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/ZCore@1.9.2.0?branch=main\u0026target=TC3.1\u0026configuration=Debug",
"hashes": [
{
"alg": "SHA-256",
"content": "ead735a8b261553f27c19ae9c942b0f043b6969430006eac33e5bd0f8ce03756"
}
],
"externalReferences": [
{
"type": "website",
"url": "https://doc.zeugwerk.dev"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "twinpack:branch",
"value": "main"
},
{
"name": "twinpack:target",
"value": "TC3.1"
},
{
"name": "twinpack:configuration",
"value": "Debug"
}
]
},
{
"type": "library",
"bom-ref": "Zeugwerk GmbH/ZEquipment@1.9.2.0",
"name": "ZEquipment",
"version": "1.9.2.0",
"description": "The Zeugwerk Equipment Library is a comprehensive and specialized module within the Zeugwerk Framework, dedicated to empowering developers with the tools needed to efficiently control a wide array of industrial equipment.",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://doc.zeugwerk.dev"
]
},
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"licenses": [
{
"license": {
"name": "ZF1000 (OEM License)"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/ZEquipment@1.9.2.0?branch=main\u0026target=TC3.1\u0026configuration=Debug",
"hashes": [
{
"alg": "SHA-256",
"content": "19a2b0ced208a7e13504e45861fedca8dcdd56c3721f9f9b655f037c94c95e23"
}
],
"externalReferences": [
{
"type": "website",
"url": "https://doc.zeugwerk.dev"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "twinpack:branch",
"value": "main"
},
{
"name": "twinpack:target",
"value": "TC3.1"
},
{
"name": "twinpack:configuration",
"value": "Debug"
}
]
},
{
"type": "application",
"bom-ref": "Zeugwerk GmbH/Zeugwerk_Quickstart@1.9.0.1",
"name": "Zeugwerk_Quickstart",
"version": "1.9.0.1",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH"
},
"licenses": [
{
"license": {
"name": "Zeugwerk End User License Agreement"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/Zeugwerk_Quickstart@1.9.0.1",
"hashes": [
{
"alg": "SHA-512",
"content": "6a821c7287b4ed3ae59acfc944e9370e681f584a8b95fad15e55fd7ccfb03d831ad39f644d2d8927c0cfd5c9a15b524d9e8a9621ab0de69bd3910c52cfd15dd3"
}
],
"properties": [
{
"name": "executable",
"value": "executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "filename",
"value": "Port_851.app"
},
{
"name": "archive",
"value": "no archive"
},
{
"name": "twincat:boot-project",
"value": "_Boot/TwinCAT RT (x64)"
},
{
"name": "twincat:platform",
"value": "TwinCAT RT (x64)"
},
{
"name": "twincat:compiler-version",
"value": "3.1.4026.20"
},
{
"name": "twincat:ads-port",
"value": "851"
},
{
"name": "twincat:application-match",
"value": "project-name"
}
]
},
{
"type": "library",
"bom-ref": "Zeugwerk GmbH/ZPlatform@1.9.2.0",
"name": "ZPlatform",
"version": "1.9.2.0",
"description": "The Zeugwerk Platform Library serves as a pivotal component within the Zeugwerk Framework, tailored to ensure seamless accessibility and compatibility across diverse industrial platforms.",
"scope": "required",
"manufacturer": {
"name": "Zeugwerk GmbH",
"url": [
"https://doc.zeugwerk.dev"
]
},
"authors": [
{
"name": "Zeugwerk GmbH"
}
],
"licenses": [
{
"license": {
"name": "ZF1000 (OEM License)"
}
}
],
"purl": "pkg:twinpack/Zeugwerk%20GmbH/ZPlatform@1.9.2.0?branch=main\u0026target=TC3.1\u0026configuration=Debug",
"hashes": [
{
"alg": "SHA-256",
"content": "468871513b83b3813b48d1d4d9475ee55fc6460f66c18e7b889cc34218f1cf04"
}
],
"externalReferences": [
{
"type": "website",
"url": "https://doc.zeugwerk.dev"
}
],
"properties": [
{
"name": "executable",
"value": "non-executable"
},
{
"name": "structured",
"value": "structured"
},
{
"name": "twinpack:branch",
"value": "main"
},
{
"name": "twinpack:target",
"value": "TC3.1"
},
{
"name": "twinpack:configuration",
"value": "Debug"
}
]
}
],
"dependencies": [
{
"ref": "Zeugwerk_Quickstart@1.9.0.1",
"dependsOn": [
"Zeugwerk GmbH/Zeugwerk_Quickstart@1.9.0.1"
]
},
{
"ref": "Zeugwerk GmbH/Zeugwerk_Quickstart@1.9.0.1",
"dependsOn": [
"Beckhoff Automation GmbH/Tc3_Module@3.4.7.0",
"Zeugwerk GmbH/ZApplication@1.9.2.0",
"Zeugwerk GmbH/ZAux@1.9.2.0",
"Zeugwerk GmbH/ZCore@1.9.2.0",
"Zeugwerk GmbH/ZEquipment@1.9.2.0",
"Zeugwerk GmbH/ZPlatform@1.9.2.0"
]
},
{
"ref": "Beckhoff Automation GmbH/Tc3_Module@3.4.7.0",
"dependsOn": []
},
{
"ref": "Zeugwerk GmbH/ZApplication@1.9.2.0",
"dependsOn": []
},
{
"ref": "Zeugwerk GmbH/ZAux@1.9.2.0",
"dependsOn": []
},
{
"ref": "Zeugwerk GmbH/ZCore@1.9.2.0",
"dependsOn": []
},
{
"ref": "Zeugwerk GmbH/ZEquipment@1.9.2.0",
"dependsOn": []
},
{
"ref": "Zeugwerk GmbH/ZPlatform@1.9.2.0",
"dependsOn": []
}
]
}
Where this fits in a real pipeline
If you already run the three-job build/staging/deploy shape from our CI/CD series, the SBOM belongs next to the artifact it describes: generated in staging when the release candidate is versioned, held with everything else staging produces, and carried forward - not regenerated - when deploy promotes that candidate. A document that was regenerated after promotion no longer proves it describes what was actually shipped.
Where to start
If you are on zkbuild-action already, you have this - check the CycloneDX file next to your .library artifacts. If you build on your own agents, zkmake sbom is included with the zkmake license. The command reference is in the zkmake documentation. Either way, the machine to run this on is the one you have today, not the one you plan to have in December 2027.
