Generate SBOMs with client tools
Practical CycloneDX workflows for Maven, Gradle, Poetry, npm, and OCI clients, including when Craftifact can bind the SBOM automatically.
Quick path
- Generate SBOMs as CycloneDX JSON.
- Prefer the sidecar pattern where the client ecosystem supports it cleanly.
- Otherwise publish the package or image first and upload the SBOM through the SBOM upload API.
- Use
artifact_idfor package artifacts andoci_digestplusrepositoryfor OCI content. - Treat a
201response as accepted and queued for parsing.
Which pattern fits which client
- Maven: best fit today. Upload a
-cyclonedx.jsonsidecar in the same GAV path as the primary artifact. - Gradle publishing to Maven repositories: same pattern as Maven. Attach the CycloneDX JSON as a classified sidecar.
- Poetry, Twine, and npm: generate CycloneDX JSON locally, publish normally, then upload the SBOM directly to Craftifact.
- Docker and OCI: push the image first, generate the CycloneDX JSON from the pushed image, then upload it by OCI digest.
- pip and Go are intentionally not covered here. The current docs show install or proxy flows there, not a normal hosted publish workflow.
Common upload endpoint
Craftifact accepts CycloneDX JSON at the SBOM upload API reference:
https://packages.example.com/api/sbom/upload/
Use exactly one target reference:
-
artifact_idfor package artifacts oci_digesttogether withrepositoryfor OCI content
For package ecosystems such as Python and npm, the artifact ID is easiest to pass from your automation. If you work manually in the UI, you can also take it from the artifact detail URL.
Keep the exact request contract, response shapes, and copy-ready upload examples in the SBOM upload API reference. The workflow sections below only add the ecosystem-specific steps around that shared endpoint.
Maven
For Maven, the smoothest path is to generate the CycloneDX JSON during the build and deploy it as a classified sidecar in the same repository transaction.
<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>PLUGIN_VERSION</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeBom</goal>
</goals>
<configuration>
<outputFormat>json</outputFormat>
<schemaVersion>1.6</schemaVersion>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
The generated file is attached as artifact-version-cyclonedx.json. That naming lets Craftifact recognize the SBOM sidecar and bind it to the matching primary artifact in the same GAV path.
mvn deploy \
-s settings.xml \
-DaltDeploymentRepository=craftifact::https://packages.example.com/repository/maven/libs-release/
Gradle
If Gradle publishes into a Maven-style hosted repository, use the same sidecar idea: run a CycloneDX task and attach the resulting JSON with classifier cyclonedx.
plugins {
id("maven-publish")
id("org.cyclonedx.bom") version "PLUGIN_VERSION"
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifact(layout.buildDirectory.file("reports/cyclonedx/bom.json")) {
builtBy(tasks.named("cyclonedxBom"))
classifier = "cyclonedx"
extension = "json"
}
}
}
}
The exact output path depends on your plugin configuration. What matters for Craftifact is the published result: a -cyclonedx.json file beside the main Maven artifact.
Poetry and Twine
For Python publishing, the common pattern is two-step:
- build and publish the package as usual
- generate a CycloneDX JSON document separately and upload it to Craftifact for the published artifact
poetry build
poetry publish --repository craftifact
One practical way to generate the SBOM is cyclonedx-py:
cyclonedx-py poetry \
--output-format json \
--output-file dist/example-cyclonedx.json
If you publish with Twine instead of poetry publish, the upload step stays the same:
twine upload \
--repository-url https://packages.example.com/repository/python/libs-release/ \
dist/*
After the package artifact exists in Craftifact, copy its Artifact ID from the artifact details panel or resolve it through the Artifact lookup API. Then upload the SBOM with that
artifact_id
:
curl \
-X POST \
-H "Authorization: Bearer $CRAFTIFACT_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @dist/example-cyclonedx.json \
"https://packages.example.com/api/sbom/upload/?artifact_id=ARTIFACT_ID"
npm
For npm, publish first and generate the SBOM with the npm client itself if your npm version supports npm sbom.
npm publish --registry https://packages.example.com/repository/npm/libs-release/
npm sbom --sbom-format cyclonedx > package-cyclonedx.json
Then upload the resulting CycloneDX JSON to the published artifact:
curl \
-X POST \
-H "Authorization: Bearer $CRAFTIFACT_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @package-cyclonedx.json \
"https://packages.example.com/api/sbom/upload/?artifact_id=ARTIFACT_ID"
If your npm setup does not expose npm sbom, use another CycloneDX-capable generator in CI and keep the Craftifact upload step unchanged.
Docker and OCI
Docker is still the normal client for build and push, but the practical SBOM pattern today is separate:
- push the image
- generate a CycloneDX JSON document from the pushed image
- upload that document to Craftifact by OCI digest
docker push packages.example.com/libs-release/demo/app:1.0.0
One straightforward generator is syft:
syft registry:packages.example.com/libs-release/demo/app:1.0.0 \
-o cyclonedx-json=image-cyclonedx.json
Use the manifest digest from your push output or from a follow-up inspect step, then upload the SBOM:
curl \
-X POST \
-H "Authorization: Bearer $CRAFTIFACT_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @image-cyclonedx.json \
"https://packages.example.com/api/sbom/upload/?oci_digest=sha256:DIGEST&repository=libs-release"
This keeps the Docker workflow familiar while still letting Craftifact store and process the SBOM for the pushed OCI content.
Uploaded and generated SBOMs
Craftifact can work with SBOMs from two sources:
- user-supplied CycloneDX JSON that you upload or publish as a sidecar,
- Craftifact-generated SBOMs for supported hosted content.
Generated SBOMs currently cover hosted Maven .jar, .war, and .ear artifacts and hosted OCI manifests or indexes.
Proxy repositories are skipped, and user-supplied SBOMs take precedence when one is already bound to the same subject.
Generated SBOMs can show freshness state over time. If the generator version or vulnerability database becomes stale, treat the result as triage context and refresh before using it as a final answer.
Related pages
- To inspect indexed components in the explorer, see Explore dependencies.
- To review vulnerability findings for one subject, see Review vulnerability findings.
- For the exact request and response contract, see SBOM upload API.
- For repository URLs and auth setup, see Configure client tools for package formats.
- For general package-family behavior, see Package format notes.
- For token issuance, see Create a robot account and use its token.