SBOM per Release
- XWiki
- Design
- Completed
- Yes
Description
Goal
Publish a machine-readable Software Bill of Materials (SBOM) with every XWiki release, so that:
- users can answer "is XWiki X.Y affected by CVE-Z?" without reverse-engineering the WAR;
- companies placing XWiki-based products on the EU market can meet the SBOM obligation of the Cyber Resilience Act (CRA) for the XWiki part (see below);
- vulnerability scanners report fewer false positives and fewer false negatives (with VEX documents, later).
What the CRA requires
From the text of Regulation (EU) 2024/2847 (we're not lawyers, this is a reading of the text, not legal advice):
- Who: the SBOM is an obligation of manufacturers (Annex I, Part II, point 1). Free and open source software that isn't monetised by its manufacturer is not considered to be supplied in the course of a commercial activity (recital 18), and open-source software stewards (Article 24) have a lighter regime with no SBOM obligation. In practice, the SBOM matters most to the companies integrating or selling XWiki, who need one covering the XWiki components (and Annex II, point 8(f), asks components intended for integration to provide integrators with what they need). Which entity, if any, is the manufacturer or steward of XWiki is not decided by this proposal.
- What: "a software bill of materials in a commonly used and machine-readable format covering at the very least the top-level dependencies of the products" (Annex I, Part II, point 1), defined as "a formal record containing details and supply chain relationships of components" (Article 3, point 39).
- Format: not prescribed. The Commission may specify the format and elements by implementing acts (Article 13(24)); none had been adopted as of October 2026. The harmonised standard on vulnerability handling (prEN 40000-1-3) is still being finalised and is expected to reference the existing formats (CycloneDX, standardised as ECMA-424, and SPDX, standardised as ISO/IEC 5962) rather than define a new one.
- Where: part of the technical documentation (Annex VII, point 2(b)), provided to a market surveillance authority on a reasoned request (Annex VII, point 8). Manufacturers are not obliged to make it public (recital 77), and there is no requirement to ship it inside the product. If the manufacturer does make it available to users, the user information must say where it can be accessed (Annex II, point 9).
- When: from 11 December 2027 for products placed on the market from that date (Article 71(2) and Article 69(2)). The vulnerability reporting obligations (Article 14) apply earlier, from 11 September 2026.
This proposal goes beyond the minimum: full transitive dependencies (not only top-level ones), the JavaScript included, and publicly available. Publishing it alongside the artifacts and linking it from the download page also covers the Annex II, point 9 case.
Vocabulary
- SBOM: a machine-readable inventory of the components a piece of software contains, with their versions and identifiers. Not to be confused with a Maven "BOM" POM.
- Artifact SBOM: the SBOM published alongside one released Maven artifact, describing that artifact and everything it transitively brings in.
- Distribution: a released artifact meant to be run as-is (e.g. the WAR). Its Artifact SBOM describes the running product: what the file contains plus what its flavor installs on first start (XARs), but not what the servlet container provides.
- Supplier: the party an SBOM names as having produced an artifact. For core and contrib artifacts this is the XWiki open source project (which has its own governance, independent of XWiki SAS).
- Bundled Component: a third-party component whose code is physically inside another artifact (e.g. an npm package compiled into a webjar), identified in its own ecosystem's terms (pkg:npm/..., not pkg:maven/...).
- Embedded SBOM: an SBOM packaged inside an artifact, listing its Bundled Components.
Proposal
Summary
- Generate a CycloneDX JSON SBOM for every deployed Maven artifact, using the cyclonedx-maven-plugin in the release profile of the XWiki Commons top level POM (so Commons, Rendering, Platform and contrib extensions all get it).
- Make the JavaScript visible: XWiki-built webjars embed an SBOM of the npm packages they bundle (generated with pnpm sbom), and a small XWiki Maven mojo folds Embedded SBOMs (and third-party webjars mapped to their npm identity) into the Artifact SBOMs, all the way up to the WAR.
- Link the Distribution SBOM from the download page and the release notes.
- Target: complete by 18.10.2 (the next cycle LTS), developed on master and backported to stable-18.10.x.
What gets an SBOM
One Artifact SBOM per deployed artifact, attached with the cyclonedx classifier (e.g. xwiki-platform-oldcore-18.10.0-cyclonedx.json), rather than one aggregate SBOM per repository.
Rationale: the unit XWiki ships is the extension (the Extension Manager installs JARs and XARs individually), not the Maven reactor, which nobody deploys. The WAR's own Artifact SBOM then answers the question about the standard distribution.
When
Only in the release profile (activated by performRelease), next to the sources, javadoc and GPG signature.
- Normal developer and CI builds are not slowed down.
- Breakages are still detected early: the existing TestRelease CI job builds with -DperformRelease=true on every build.
- The plugin is configured in pluginManagement so that anyone can still run mvn cyclonedx:makeBom by hand.
- Contrib extensions inherit it with their next parent upgrade. A property allows skipping it for exotic builds.
Format
CycloneDX JSON only (no XML, no SPDX for now).
- All the consuming tools (Dependency-Track, Trivy, Grype, OSV-Scanner, GitHub) read CycloneDX JSON.
- The CRA does not mandate a format (see above). The German BSI TR-03183-2, the most concrete CRA-aligned SBOM specification so far, accepts CycloneDX or SPDX, in JSON or XML.
- VEX (needed to cut false positives) is native to CycloneDX, and its tooling support is much better than for SPDX.
- Generating SPDX as well would double everything (plugins, attached and signed files, and the JavaScript handling below). It can be added later, either with the spdx-maven-plugin or by conversion, once we know what is actually needed. We know of at least one SPDX request, so this is parked, not rejected.
Content and metadata
- includeProvidedScope=false: provided dependencies (e.g. the Servlet API) are supplied by the container, not by XWiki, and including them produces false positives (e.g. a Tomcat CVE "affecting" XWiki).
- projectType=application for Distributions (the default is library).
- XARs pulled by the WAR are kept, even though the WAR excludes the XAR files themselves, since the flavor installs them: the Distribution SBOM describes the running product.
- The Supplier stays XWiki / https://xwiki.org/, i.e. the project (XWiki SAS products already override the POM <organization> with XWiki SAS). The mojo below writes it to metadata.supplier and metadata.manufacturer, which the CycloneDX plugin can't set.
- XWiki's license is declared with its SPDX identifier, LGPL-2.1-or-later (the current "LGPL 2.1" name risks being resolved to LGPL-2.1-only, which is wrong since our headers say "or (at your option) any later version").
- The timestamp is derived from project.build.outputTimestamp, so builds stay reproducible.
JavaScript: XWiki-built webjars
Several XWiki webjars bundle npm packages with Vite (BlockNote, Live Data, the xwiki-platform-node packages, etc.). Maven knows nothing about that code, and the WAR SBOM is computed from the Maven dependency tree, so even if a webjar listed its npm packages, the WAR's SBOM would not.
Proposal:
- Each XWiki-built webjar runs pnpm sbom --sbom-format cyclonedx --prod --filter <its workspace package> (native in pnpm since 11.0; we use 11.28.4) and embeds the result in its JAR, as META-INF/sbom/npm.cdx.json (the META-INF/sbom/ directory is the convention used by Spring Boot and recommended by the CycloneDX Maven plugin maintainers). A normalize goal of the XWiki Commons tool Maven plugin below makes it reproducible first, since pnpm sbom writes the current date, a random serial number, and a component order and dependency graph that vary from one run to the next. It's set up once, in a profile of the platform root POM activated by src/main/node/package.json, and runs in every build (less than a second per module) so that the CI verifies it.
- A new goal in an XWiki Commons tool Maven plugin runs after makeBom. For the module itself and for each resolved dependency JAR, it reads any Embedded SBOM and adds its components to the module's Artifact SBOM as Bundled Components contained in that JAR.
Result: the webjar's Artifact SBOM lists its npm packages, and so does the WAR's (the JARs are already resolved locally: no network access, no extra classifiers to download). Any third-party JAR embedding an SBOM is supported for free.
Choices made:
- Lockfile-based (pnpm sbom --prod), not bundle-based. Over-reports the packages that Vite tree-shakes away or keeps external (e.g. vue for Live Data, which is also listed as a webjar), which is the safe failure mode, and which VEX not_affected statements handle later. A bundle-based generator (Rollup/Vite plugin) can be used for a specific webjar later if over-reporting becomes a problem.
- Not cyclonedx-cli merge (a .NET binary in every release build, and it doesn't solve the WAR level), and not separate per-ecosystem SBOM files (which leave the WAR SBOM blind, i.e. fail the main goal).
- A flat Embedded SBOM graph: the module package depends on every bundled package. pnpm sbom walks the packages concurrently and records a package reached from several parents under one of them only, so its graph is both incomplete and not reproducible; what matters for a webjar is that it bundles them all.
- A flat list, not nested components. Each Bundled Component is a top-level component, which the JAR containing it depends on (dependencies edges). A prototype showed that Trivy (0.76) ignores nested components, while Grype and Dependency-Track read both layouts. A Bundled Component's bom-ref is the JAR's bom-ref, !/ and the npm purl, e.g. pkg:maven/org.webjars/[email protected]?type=jar!/pkg:npm/[email protected].
JavaScript: third-party webjars
Third-party webjars (org.webjars, org.webjars.npm) currently appear only as pkg:maven/org.webjars/jquery@..., so scanners miss npm advisories (e.g. for jQuery or Bootstrap).
The same mojo adds the npm package each webjar contains as a Bundled Component (pkg:npm/<name>@<version>), keeping the webjar's own Maven component:
- org.webjars.npm: mechanical mapping (same name and version).
- Classic org.webjars: a small curated mapping table in the tool (artifact id to npm name, version normalization). Unmapped webjars stay Maven-only and the build logs a warning, so that the table is updated when a new webjar is added.
We could also suggest to the WebJars project to embed SBOMs upstream (optional; it would make the mapping table unnecessary over time).
Discoverability
The Distribution SBOM is linked from the xwiki.org download page and from each release notes page (template change). It is not copied inside the WAR: the per-instance SBOM follow-up below covers "the SBOM of what I'm running" better, including installed extensions.
VEX readiness
VEX (Vulnerability Exploitability eXchange) documents are a follow-up, but published SBOMs are immutable, so the SBOMs must not get in their way:
- Stable component identifiers. VEX statements designate components by purl (or, for CycloneDX VEX, by bom-ref within a given SBOM). Every component, Bundled Components included, gets its ecosystem purl (pkg:maven/..., pkg:npm/...), and the mojo generates deterministic and unique bom-refs. The same npm package bundled in two webjars appears twice in the WAR SBOM with the same purl, so its bom-ref combines the containing JAR's purl and the npm purl, while the purl itself stays the plain npm one so that matching by purl keeps working.
- VEX documents are published separately, at a location that can be updated (e.g. one per supported release line), never attached to the release artifacts: statements are written after a release, as new CVEs get assessed.
- No link from the SBOM to the VEX for now. CycloneDX supports an exploitability-statement external reference, which would have to be reserved now since released SBOMs can't be changed. Rejected for now: tools import VEX next to the SBOM anyway, and it would mean a dead link in every artifact until VEX exists.
The VEX format (CycloneDX VEX, OpenVEX or CSAF) is left open: all of them identify components by purl, so choosing CycloneDX for the SBOM doesn't constrain it.
Testing
- Unit tests for the new mojo (folding of Embedded SBOMs, webjar to npm mapping, warnings).
- A verification goal run in the WAR module under the release profile (thus by platform's TestRelease CI job), asserting a configurable list of expected purls (e.g. pkg:npm/jquery, some Live Data Bundled Components) and forbidden ones (e.g. the Servlet API).
Schedule
- Maven-only part (POM configuration): 18.10.0 or 18.10.1.
- Embedded SBOMs, webjar mapping and verification: master first, backported to stable-18.10.x, so that 18.10.2 (the cycle LTS) is complete.
- Release notes template and download page link: from the first release publishing a Distribution SBOM.
Implementation
The proposal was accepted on the forum. The work is tracked by:
- XCOMMONS-3864: Generate a CycloneDX SBOM for every released Maven artifact
- XCOMMONS-3865: Fold bundled npm packages and third-party webjars into the artifact SBOMs (depends on XCOMMONS-3864), in PR #2107. On a platform module, the Grype and Trivy findings went from 0 to 5 thanks to the third-party webjar mapping.
- XWIKI-25619: Embed an SBOM of the bundled npm packages in XWiki webjars (depends on XCOMMONS-3865, which also brings the normalize goal), in PR #7285. The Live Data webjar embeds 50 npm packages, and two builds give byte-identical JARs.
- XWIKI-25620: Publish an SBOM of the XWiki WAR with each release (depends on XCOMMONS-3865 and XWIKI-25619)
Prior art and evidence
Other Java projects shipping JavaScript
| Project | SBOM? | JavaScript handling |
|---|---|---|
| Keycloak | Open PR keycloak#51743 | pnpm sbom --prod + an assembly-level handler merging the npm and Java SBOMs. Closest to this proposal. |
| Jenkins | Open PR jenkins#26703 | Maven only, the bundled JavaScript isn't covered. |
| Dependency-Track | Yes, per component | Separate frontend SBOM (webpack plugin), not merged into the backend's. |
| Apache Maven parent | Yes, apache-release profile | Maven only (aggregate SBOM). |
| Apache Airflow, Eclipse Theia | Yes | One flat polyglot SBOM generated by cdxgen. |
Relevant upstream discussions: the CycloneDX Maven plugin cannot merge external or npm SBOMs (#452, #683), webjars only get Maven purls (#680), and scanners miss webjar CVEs as a result (grype#2014). pnpm sbom was added in pnpm#10592.
Why not scan the WAR instead?
Binary scanners (Syft, Trivy, cdxgen) can generate an SBOM from a WAR without Maven. Running Syft on xwiki-platform-distribution-war-18.9.0-SNAPSHOT.war gave:
- 837 components, 836 pkg:maven, 0 pkg:npm: nothing of the npm code bundled in the XWiki webjars (e.g. Live Data) was found, and the 50 third-party webjars appear as pkg:maven only;
- no XARs (not in the file) and essentially no dependency graph (6 edges for 837 components).
These tools identify JARs through META-INF/maven//pom.properties and JavaScript through package.json / lock files, which a Vite bundle doesn't keep. So a scanner only covers Distributions (not the individual extensions), loses information, and still needs the Embedded SBOMs. It remains useful as a cross-check (see follow-ups).
Out of scope / follow-ups
- Docker images: the image is built by Docker's official images infrastructure, on its own schedule, and Docker already attaches SBOM attestations to official images. Follow-up: have the Dockerfile template also download the WAR's SBOM (with a sha256 check) into the image, so that scanners such as Trivy use it.
- Syft cross-check in CI: fail if Syft finds a component in the WAR that our Distribution SBOM lacks.
- CVE scanning on CI based on the SBOM (suggested on the forum): once the Distribution SBOM exists, have the CVE scanning run on CI use it, so that it also covers the JavaScript part.
- VEX documents, to declare which CVEs reported on our components don't affect XWiki.
- Instance SBOM: an SBOM exported by the Extension Manager, describing a running instance (the Distribution plus all installed extensions, at their installed versions). The .xed descriptors already present next to each JAR in WEB-INF/lib are a good starting point.
- SPDX, when the actual need is known.
- Bundle-based npm lists for webjars where lockfile over-reporting hurts.
Vincent Massol