From Internal Tool to TYPO3 Product: What Extension Manufacturers Should Know About SBOMs
Turning an internal tool into a commercial TYPO3 extension makes you a software manufacturer — and the Cyber Resilience Act expects manufacturers to know what their product contains. A practical look at Software Bills of Materials for the PHP and frontend parts of a TYPO3 extension.
At Digital Zombies, we work on TYPO3 upgrades continuously. Over time, we have developed our own processes, scripts and technical knowledge to make these projects more predictable. It was therefore natural for us to consider whether some of this tooling could become a TYPO3 extension that other agencies could use.
From an engineering perspective, the initial questions were familiar. What should the extension automate? Which TYPO3 versions should it support? How could we integrate it into existing projects without introducing unnecessary complexity? How would we test and maintain it?
Then another question appeared:
If we turn an internal tool into a commercial TYPO3 extension, are we simply selling useful code — or are we becoming the manufacturer of a software product?
This distinction is becoming increasingly important because of the European Cyber Resilience Act, or CRA. The CRA introduces cybersecurity requirements for products with digital elements made available on the EU market. Depending on how it is developed, marketed and distributed, a commercial TYPO3 extension may qualify as such a product. Whether the CRA applies to a typical TYPO3 website project in the first place is a separate question, which I examine in a companion article on TYPO3 website projects and the CRA.
This article is not intended to determine the legal classification of every TYPO3 extension. I am approaching the subject as the CEO of a TYPO3 engineering company, not as a lawyer. Instead, I want to concentrate on one practical engineering consequence that commercial extension developers should understand: the Software Bill of Materials, usually abbreviated as SBOM.
What does an SBOM contain? How can we create one for the PHP and frontend parts of a TYPO3 extension? And how can we connect that information with releases, vulnerability notifications and the resulting engineering decisions?
When internal code becomes a product
The same TYPO3 extension can exist in very different commercial situations.
An agency may develop an extension exclusively for a customer project. A company may create an internal tool for its own developers. An extension author may publish code freely without monetising it. Alternatively, the extension may be licensed, sold and supported as a standardised product for many customers.
From a technical perspective, the code may look almost identical. From a commercial and potentially legal perspective, the situations are different.
When we considered turning parts of our internal upgrade tooling into a reusable extension, we had to look beyond its immediate functionality. Selling software means taking responsibility not only for whether the current version works, but also for how it is developed, updated and supported.
This introduces questions such as:
- How long will we support each release?
- How do we learn about vulnerabilities in our dependencies?
- How quickly can we determine which versions are affected?
- How do we distribute security updates?
- What happens when a dependency is no longer maintained?
- How do we document the decisions made during development?
These are reasonable engineering questions even without the CRA. The regulation makes them more difficult to postpone.
One of the first requirements is knowing what the product actually contains. That is where the SBOM becomes relevant.
What is an SBOM?
A Software Bill of Materials is a structured inventory of the software components contained in a product. It is often compared to a list of ingredients, although a software application is more complicated because its components depend on one another.
An SBOM can include:
- component and package names;
- exact versions;
- package suppliers or authors;
- package identifiers;
- licence information;
- checksums;
- direct dependencies;
- transitive dependencies;
- relationships between components.
The result is normally stored in a standard, machine-readable format. Two established formats are CycloneDX and SPDX. For the examples in this article, I use CycloneDX because its model is designed around software supply-chain and vulnerability-management use cases.
CycloneDX can represent components, services, dependency relationships, vulnerabilities and information about the completeness of the inventory. Its current specification is available at cyclonedx.org/specification/overview.
For a TYPO3 extension, this matters because even a relatively small product may contain more than its own PHP classes. It can depend on TYPO3 Core packages, other extensions, Symfony components, external PHP libraries and npm packages used for its backend module or frontend output.
Without a structured inventory, answering a basic security question can take longer than expected:
Does a version of our extension used by customers contain the affected component?
An SBOM makes the inventory machine-readable, allowing it to be stored, compared and connected to vulnerability-monitoring systems.
What does the CRA say about SBOMs?
The Cyber Resilience Act includes SBOM creation among the vulnerability-handling requirements for manufacturers. Annex I, Part II requires manufacturers to identify and document vulnerabilities and components contained in their products, including by drawing up an SBOM in a commonly used, machine-readable format. It must cover at least the product’s top-level dependencies.
The complete regulation is available on EUR-Lex.
This does not mean that every SBOM must be made publicly available. It also does not mean that creating an SBOM is sufficient to demonstrate CRA compliance. It is one part of a broader process that includes secure product development, vulnerability handling, documentation, security updates and incident reporting.
For an extension developer, the meaningful engineering question is therefore not simply how to create a particular file. It is whether we can reliably identify the components contained in each released version and react when one of them becomes vulnerable.
What is the product we are documenting?
Before generating anything, we need to define the product boundary.
This sounds obvious, but it is one of the most important decisions in the process. A commercial TYPO3 extension and the complete TYPO3 installation in which it is used are not the same product.
Imagine that we sell an extension called vendor/upgrade-assistant. Its composer.json might contain:
{
"name": "vendor/upgrade-assistant",
"description": "Upgrade analysis and assistance tools for TYPO3",
"type": "typo3-cms-extension",
"require": {
"php": "^8.2 || ^8.3 || ^8.4",
"typo3/cms-core": "^13.4 || ^14.0",
"symfony/console": "^7.2"
}
}
A reusable Composer package should normally not contain a manually maintained "version" field. Composer can derive the release version from the Git tag or repository metadata.
The extension declares which versions it supports, but it does not determine which exact TYPO3 or Symfony versions will be installed in a customer project. Composer resolves those versions together with the requirements of the complete installation.
This leaves us with two different inventories.
The extension-level SBOM
The extension-level SBOM describes the product we distribute. It should identify:
- the extension and its release version;
- its declared Composer requirements;
- PHP libraries bundled with the extension;
- JavaScript libraries included in compiled assets;
- other third-party code distributed inside the package.
This is the product boundary over which the extension manufacturer has the most direct control.
The project-level SBOM
The project-level SBOM is generated from the complete installed TYPO3 project. It can contain:
- the exact installed TYPO3 Core version;
- the commercial extension;
- all other installed extensions;
- the complete resolved Composer dependency tree;
- customer-specific packages;
- project-level frontend dependencies.
An extension manufacturer cannot generate an exact project-level SBOM for every customer because the customer’s project resolves the final package versions. Conversely, a project-level SBOM does not remove the extension manufacturer’s responsibility to understand what is distributed as part of the extension.
Generating a Composer SBOM
The CycloneDX PHP Composer plugin generates an SBOM from Composer package information. According to its documentation, it supports PHP 8.1 or newer and Composer 2.3 or newer.
It can be added as a development dependency:
composer require --dev cyclonedx/cyclonedx-php-composer
mkdir -p build
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=build/sbom-backend.cdx.json \
--omit=dev \
--spec-version=1.7 \
--validate
The command name and options above correspond to the current plugin documentation at github.com/CycloneDX/cyclonedx-php-composer.
The plugin supports CycloneDX specification versions up to 1.7 and can validate the generated document. It obtains its information from composer.json, the installed Composer setup or a working composer.lock file. Its documentation recommends using the installed setup where available.
The --omit=dev option means that development dependencies are excluded from the generated inventory. The plugin itself can therefore be used during the build without appearing as part of the distributed extension.
There is, however, an important sequencing detail. If the plugin is installed under require-dev, this will not work:
composer install --no-dev
composer CycloneDX:make-sbom
The first command omits the plugin, meaning that the second command may no longer be available.
In a release pipeline, we can instead install all dependencies in the analysis job, generate the SBOM while excluding development dependencies from its content, and then create the production artifact in a separate step:
composer install --no-interaction --prefer-dist
mkdir -p build
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=build/sbom-backend.cdx.json \
--omit=dev \
--spec-version=1.7 \
--validate
An alternative is installing the CycloneDX plugin globally in the CI environment. In that case, the project itself does not need to require the generator. Whichever method is chosen, the tool version should be fixed or recorded so that the process remains reproducible.
Setting the extension release version
In a library repository, the root component’s version may not always be detected as expected, particularly when the pipeline checks out a commit without its Git tag.
The plugin provides an explicit --mc-version option for the main component:
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=build/sbom-backend.cdx.json \
--omit=dev \
--spec-version=1.7 \
--mc-version=1.2.0 \
--validate
In a real pipeline, 1.2.0 should come from the validated release tag rather than being entered manually in several places.
The Composer limitation for reusable extensions
The generated Composer SBOM is exact for the dependency set installed in the environment where the command runs. It is not automatically an exact description of every customer installation.
Suppose the extension requires:
{
"require": {
"symfony/console": "^7.2"
}
}
The extension’s test environment might resolve Symfony Console 7.4, while a customer installation could contain another compatible version. Both satisfy the declared constraint.
For this reason, an SBOM created from the extension’s CI environment should be described accurately. It can document:
- the direct requirements declared by the extension;
- bundled third-party code;
- the exact dependency set used to test the release.
It should not be presented as proof of the exact versions running in every customer installation.
For a commercial extension supporting several TYPO3 versions, it may be useful to generate a separate test SBOM for each supported dependency matrix. These documents show which combinations were tested, while the final customer project still needs its own project-level inventory.
A TYPO3-specific option: SBOMinator
There is already an experimental TYPO3 extension for generating an SBOM directly from a TYPO3 installation: sbominator/sbom-typo3.
The extension was initiated during the CloudFest Hackathon 2025. It provides a TYPO3 backend module from which an administrator can download an inventory in SPDX or CycloneDX format.
Its package and current status can be found at packagist.org/packages/sbominator/sbom-typo3.
The documented installation command is:
composer require "sbominator/sbom-typo3:@dev"
However, this needs a clear warning. The maintainers describe the extension as experimental and as a proof of concept. They also state that the generated SBOM content still needs validation and refinement. Its current dependency declarations use development constraints, so I would evaluate it in a development or test installation rather than introduce it directly into a production project.
SBOMinator is nevertheless highly relevant because it demonstrates how a project-level inventory could be made accessible inside TYPO3. Instead of asking an administrator to run a command on the server, it provides a backend module that examines the software components used by the website and offers the result as a download.
That solves a different problem from our commercial extension example.
| Method | Primary scope |
|---|---|
| CycloneDX Composer plugin in the extension repository | Extension development and release dependency set |
| npm SBOM in the extension repository | Extension frontend dependency tree |
| SBOMinator inside TYPO3 | Components detected in a complete TYPO3 installation |
| Project pipeline SBOM | Resolved dependencies of a particular deployed release |
For an extension manufacturer, SBOMinator cannot replace the product-level release process. It runs within the customer’s complete TYPO3 environment and therefore describes a broader installation. At the same time, the manufacturer’s Composer SBOM cannot describe the customer’s entire project.
The approaches complement each other. The extension manufacturer documents what is developed and distributed. The project operator documents what is finally installed and deployed.
Avoid invisible PHP dependencies
Most TYPO3 extensions should allow Composer to manage their PHP dependencies. Copying a third-party library into the extension or distributing an internal vendor/ directory makes identification, updates and vulnerability monitoring more difficult.
If external code is copied into locations such as these, it will probably not be identified automatically:
Resources/Private/Lib/
Resources/Public/JavaScript/Vendor/
Classes/Utility/ThirdParty/
The absence of a Composer entry does not turn third-party code into original code. It merely makes its origin and version harder to trace.
When a vulnerability is later reported, the copied library may still be present even though nobody remembers where it came from. For a commercial product expected to receive security support over several years, that creates unnecessary risk.
External PHP libraries should therefore remain visible to Composer wherever practical. If bundling cannot be avoided, the component must be added to the product inventory through another controlled process.
Generating the frontend SBOM
A Composer SBOM does not describe npm packages used to build an extension’s backend module or frontend functionality.
Current npm versions provide a native npm sbom command. Assuming that package-lock.json is present and the dependencies have been installed with npm ci, a CycloneDX document for an extension can be generated with:
mkdir -p build
npm sbom \
--sbom-format=cyclonedx \
--sbom-type=library \
> build/sbom-frontend.cdx.json
For a reusable TYPO3 extension, library is the appropriate type. For a complete deployed TYPO3 project, application would generally be more accurate.
The command and available values are documented by npm at docs.npmjs.com/cli/v11/commands/npm-sbom.
If the pipeline intentionally wants to generate the document from package-lock.json without using the installed node_modules tree, npm supports:
npm sbom \
--package-lock-only \
--sbom-format=cyclonedx \
--sbom-type=library \
> build/sbom-frontend.cdx.json
This command is valid, but the result contains less metadata. npm documents that lock-file-only mode does not read information such as descriptions, homepages and engine requirements from the dependency packages.
Why --omit=dev can be misleading
npm also supports --omit=dev:
npm sbom \
--sbom-format=cyclonedx \
--sbom-type=library \
--omit=dev \
> build/sbom-frontend.cdx.json
I would not use that option automatically for a distributed TYPO3 extension.
Frontend build dependencies are commonly declared under devDependencies, but their code may still be bundled into the JavaScript delivered with the extension. Vite, webpack or esbuild can transform hundreds of npm modules into a few compiled files. The customer does not receive node_modules, but the resulting assets can still contain code originating from those packages.
A Sass compiler used only to transform SCSS is part of the build environment. A JavaScript library bundled into the distributed output becomes part of the resulting product. Both may appear under devDependencies, although they play different roles.
Package-manager classification alone therefore does not define the product boundary. The release process must determine which dependencies contribute code to the distributed artifact.
{
"name": "@vendor/upgrade-assistant",
"version": "1.2.0",
"private": true,
"dependencies": {
"lodash": "4.17.21"
}
}
npm ci
mkdir -p build
npm sbom \
--sbom-format=cyclonedx \
--sbom-type=library \
> build/sbom-frontend.cdx.json
The "private": true setting is useful when package.json exists only for building the TYPO3 extension and should not itself be accidentally published to the npm registry.
The command above was verified with npm 11.9.0. It produced a valid CycloneDX document, classified the root component as a library and included the locked dependency with its exact version.
Our planned architecture: connecting releases, SBOMs and vulnerabilities
When we considered the architecture of our upgrade-assistance extension, we did not want SBOM generation to be an isolated pipeline command that produces two files nobody looks at again.
The useful part begins when the different pieces are connected.
A commercial TYPO3 extension can contain PHP dependencies managed through Composer and frontend dependencies managed through npm. These produce separate dependency trees and therefore separate SBOM files. At the same time, both inventories describe parts of the same product release.
Our planned architecture was therefore based on a release identity that connects:
- the extension version;
- the Git commit;
- the distributed ZIP or Composer package;
- the Composer SBOM;
- the npm SBOM;
- the supported TYPO3 versions;
- known vulnerabilities;
- the result of our engineering assessment;
- security updates that replace the affected release.
A simplified release job could contain:
composer install --no-interaction --prefer-dist
npm ci
npm run build
mkdir -p build
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=build/sbom-backend.cdx.json \
--omit=dev \
--spec-version=1.7 \
--mc-version=1.2.0 \
--validate
npm sbom \
--sbom-format=cyclonedx \
--sbom-type=library \
> build/sbom-frontend.cdx.json
This example assumes that:
- the CycloneDX Composer plugin is installed as a development dependency;
composer.lockandpackage-lock.jsonare available;- npm supports the
sbomcommand; - version
1.2.0is being built; - the frontend assets are generated before the release artifact is packaged.
The result is not one SBOM, but two valid CycloneDX documents:
upgrade-assistant-1.2.0-backend.cdx.json
upgrade-assistant-1.2.0-frontend.cdx.json
Simply concatenating these files would not produce a valid combined SBOM. Instead, both documents should be connected to the same release record.
Conceptually, that record could contain:
{
"product": "vendor/upgrade-assistant",
"version": "1.2.0",
"commit": "4f82c91",
"artifacts": {
"package": "upgrade-assistant-1.2.0.zip",
"backendSbom": "upgrade-assistant-1.2.0-backend.cdx.json",
"frontendSbom": "upgrade-assistant-1.2.0-frontend.cdx.json"
}
}
This is not an SBOM format and should not be presented as one. It is an example of the metadata needed to connect the product release with its related artifacts.
The important architectural principle is that a vulnerability should never point only to an abstract package. It should lead us to the affected product releases and, from there, to the customers or installations that may require an update.
Generating the files is the easier part
Composer and npm can already provide most of the information required for a basic dependency inventory. The more difficult question is what happens when a new vulnerability is published several months after the extension release.
The SBOM files do not notify us automatically. They contain component information, but something still needs to compare that inventory continuously against current vulnerability sources.
A complete process therefore requires another layer:
- the pipeline generates the SBOMs;
- the SBOMs are stored with the product release;
- their components are imported into a monitoring system;
- the system regularly checks current vulnerability databases;
- a relevant match creates a notification;
- an engineer assesses whether the extension is actually affected;
- the assessment and resulting action are documented;
- an update is prepared and connected to the affected release.
This was also one of the most interesting architectural questions for us. Generating an SBOM once is relatively straightforward. Receiving a reliable notification six months later, when a newly discovered vulnerability affects a transitive dependency, requires a persistent system.
The monitoring platform needs to know more than the current dependency tree. It must retain historical releases because customers may still be using version 1.1.0 even after version 1.2.0 has been published.
A vulnerability may therefore result in different assessments:
| Release | Component | Result | Action |
|---|---|---|---|
| 1.0.0 | Affected dependency | Affected | Security update required |
| 1.1.0 | Affected dependency | Not exploitable | Assessment documented |
| 1.2.0 | Updated dependency | Not affected | No action required |
This historical connection is one reason why running composer audit or npm audit only against the latest main branch is not sufficient for a commercial product. Those commands can provide valuable information about the current checkout, but they do not automatically monitor every previously distributed release.
From vulnerability match to engineering decision
A vulnerability database can identify a potential match between an advisory and a package version. It cannot always determine whether the commercial extension is actually affected.
For example:
- the vulnerable function may not be used;
- the affected code path may not be reachable;
- the vulnerability may require a configuration the extension does not support;
- the dependency may be used only during the build;
- the distributed asset may not contain the affected part;
- a mitigation may already exist in the extension.
The opposite is also true. A dependency scanner cannot identify a vulnerability in our own custom PHP or JavaScript simply because that code does not have an external package advisory.
The notification is therefore the beginning of an engineering process, not its conclusion.
For every relevant match, we need to determine:
- which releases contain the component;
- whether the affected functionality is present;
- whether it can be reached in the intended use of the extension;
- whether an update or mitigation is available;
- whether a new extension release is necessary;
- which customers or installations need to be informed.
CycloneDX also supports VEX, the Vulnerability Exploitability eXchange. VEX can be used to document whether a product is affected by a known vulnerability and explain the result of the assessment: cyclonedx.org/capabilities/vex.
This is important because repeatedly dismissing scanner findings without documentation does not create a reliable security process. If we conclude that an extension is not affected, that conclusion should be connected to the vulnerability, the relevant product releases and the technical reasoning behind the decision.
The notification challenge
The vulnerability notification layer deserves particular attention because the quality of the entire process depends on it.
A notification system that generates too many irrelevant alerts will eventually be ignored. A system that only checks direct dependencies may miss a vulnerability further down the dependency tree. A system that monitors only the newest release leaves customers using supported older versions unprotected.
A useful monitoring process should therefore be able to:
- recognise standard package identifiers such as Package URLs;
- process both Composer and npm components;
- retain historical product releases;
- distinguish direct and transitive dependencies;
- avoid creating duplicate alerts for the same vulnerability;
- reflect updated or withdrawn advisories;
- assign affected products to the responsible engineering team;
- store the assessment and remediation status;
- notify again when the available information changes.
This is where an SBOM develops operational value. It gives the monitoring system a standardised inventory, but the system still needs to turn new vulnerability information into an actionable engineering task.
For a company maintaining several products, this can become a central service rather than a separate process in every repository. Each release pipeline sends its SBOMs to the platform, while the platform handles continuous monitoring, notifications and documented assessments.
Where SBOMinator fits into this architecture
The experimental sbominator/sbom-typo3 extension approaches the same subject from the perspective of the installed website. It can generate CycloneDX or SPDX inventories from inside a TYPO3 installation.
This provides another useful input for the architecture.
The extension manufacturer’s pipeline describes the dependencies connected to a particular product release. SBOMinator attempts to describe the components present in the customer’s complete TYPO3 installation.
These perspectives can be connected:
- the manufacturer knows which dependencies belong to the commercial extension;
- the project operator knows which exact package versions were resolved in the installation;
- the monitoring platform knows which releases and installations may be affected;
- the engineering assessment determines whether action is required.
SBOMinator is currently described as an experimental proof of concept, so its output still needs to be validated before it becomes part of a production compliance process. Nevertheless, it demonstrates how TYPO3 itself can participate in a larger software-inventory architecture.
The interesting long-term possibility is not only downloading an SBOM manually from the TYPO3 backend. It is securely transmitting the inventory to a central platform that can monitor it throughout the lifetime of the installation.
More is required from software manufacturers today
Before the Cyber Resilience Act, a commercial extension developer could concentrate mainly on developing the functionality, publishing new versions and responding when customers reported problems.
That is no longer sufficient for a professionally maintained software product.
A manufacturer increasingly needs a structured process for:
- identifying all components contained in the product;
- monitoring vulnerabilities after a release;
- assessing whether the product is affected;
- providing security updates;
- documenting technical decisions;
- defining and communicating a support period;
- notifying the appropriate parties when necessary.
This means more work than was commonly expected from a TYPO3 extension developer before the CRA. It also means that maintenance and security responsibilities need to be considered when defining the price and support model of a commercial extension.
However, I do not see the underlying goal as unreasonable. A customer buying a software product should be able to expect that its manufacturer knows which components it contains, follows newly published security information and can provide an update when a relevant vulnerability is discovered.
The CRA formalises responsibilities that mature software manufacturers should already take seriously. Its implementation will create technical and organisational challenges, especially for smaller extension vendors, but the intention behind these requirements is understandable.
Conclusion
Creating a commercial TYPO3 extension today involves more than developing a useful feature and publishing a package.
The backend and frontend dependencies need to be identified. The generated SBOMs need to be connected to a concrete release. Historical releases need to remain visible. Vulnerability information must be monitored continuously, and an alert must lead to a documented engineering assessment and, where necessary, a security update.
This was the architectural direction we considered for our own extension:
Connect every product release with its backend and frontend SBOMs, monitor those inventories continuously, and turn relevant vulnerability information into an actionable engineering process.
The Cyber Resilience Act requires software manufacturers to do more than many extension developers did before 2024. That creates additional effort, but it also has a valuable goal: products should not become invisible to their manufacturers after they have been sold. We agree with that principle.
An SBOM does not make software secure on its own, but when it is connected to releases, vulnerability monitoring, notifications and engineering decisions, it becomes the foundation of a more responsible product lifecycle.