SBOMs (Software Bill of Materials)
An SBOM (Software Bill of Materials) is the per-build ingredient list — every direct and transitive dependency baked into an artifact, with versions and hashes. fremforge stores SBOMs your CI produces so the inventory survives across builds, image rotations, and dependency drift.
When SBOMs are generated automatically
There is nothing to install. fremforge generates a CycloneDX 1.6 SBOM centrally for every pull request, on its own runners, and binds that SBOM to a release when you publish one — any tag, including the usual semver / calver shapes (v1.2.3, v2026.06).
We deliberately do not re-scan at release time. An SBOM is a function of the source tree, and the tree you release has already been scanned as the head of the pull request that merged it, so generating a second one would produce a byte-identical file. Publishing a release instead marks the SBOM already held for that exact commit as the release SBOM. Two consequences worth knowing:
- It is immediate. No build runs, so the release SBOM is available as soon as the release is published.
- The binding is server-side. fremforge resolves your tag to its commit through Forgejo and matches on that commit, so nothing in a build can claim a release SBOM it did not earn.
If you publish a release for a commit that never went through a pull request — a tag on a very old commit, or a repository whose history predates scanning — there is no SBOM to bind. That release is reported as uncovered rather than silently skipped; re-run a scan on the commit to cover it.
Trigger an SBOM by tagging a release
From the command line:
git tag v1.2.3
git push origin v1.2.3From the Forgejo UI: open the repo → Releases → New release — set a tag name starting with v (e.g. v1.2.3) and click Publish.
A one-off SBOM for an arbitrary commit is also available via the Actions tab → SBOM (Syft) → Run workflow.
Why tag-only? Generating SBOMs on every commit produces dozens of nearly-identical manifests that nobody reads — auditors want the SBOM “at the moment we shipped release X”. Tag-only keeps the SBOM count proportional to your actual release cadence. If your team genuinely needs per-commit SBOMs (high-frequency release trains), edit the workflow’s
on:block in your repo and addpush: { branches: [main] }.
What fremforge accepts
fremforge generates both CycloneDX 1.6 and SPDX 2.3 SBOMs, and accepts both at ingest:
- CycloneDX JSON (1.6) — the inventory every fremforge feature reads (release binding, the empty-inventory check, the overview). The wider tooling ecosystem (Trivy, Syft, anchore) emits CycloneDX natively.
- SPDX JSON (2.3) — generated alongside it by the same Syft run on every PR scan, and stored under the same build, so the SBOMs page offers both downloads for one release. Your own uploads may be CycloneDX 1.5–1.7 or SPDX 2.x/3.x; the ingest validates
specVersion/spdxVersionbefore storing.
The SPDX document is an export format, generated on a best-effort basis: for a very large repository whose two documents together would exceed the 14 MiB delivery limit, only the CycloneDX SBOM is stored, so the inventory is never lost to make room for the second format.
Each SBOM is stored with:
- The SHA-256 digest of the SBOM file. The uploader sends it and the server recomputes it, so a truncated or altered upload is refused.
- An optional in-toto attestation bundle (JSONL) — Cosign-signed when your workflow signs the SBOM. The ingest validates the JSONL shape + per-line in-toto fields before storing.
- The artifact name + version, exactly as your workflow names it. Used for the de-duplication key (same digest + name + version = one row, not many).
5 MiB hard cap per SBOM. The ingest endpoint returns 413 payload-too-large above that — chunk large monorepo SBOMs by module if you hit it.
How to push an SBOM from CI
The fremforge runner image ships Syft and Trivy, so most workflows are a 3-line snippet:
- name: Generate + push SBOM
run: |
syft packages dir:. --output cyclonedx-json=sbom.json
# The digest is the SHA-256 of the SBOM file itself; the server recomputes it
# and refuses a mismatch with 400 digest-mismatch.
DIGEST=$(sha256sum sbom.json | cut -d' ' -f1)
curl -sS --fail \
-H "Authorization: Bearer ${{ secrets.FREMFORGE_API_TOKEN }}" \
-F "sbom=@sbom.json" \
-F "artifact_name=myapp" \
-F "artifact_version=${GITHUB_SHA}" \
-F "sha256_digest=${DIGEST}" \
-F "format=cyclonedx-json" \
"https://frem.sh/_app/api/v1/orgs/${ORG}/sboms"The PAT needs the sboms:write scope; mint one from Admin → API tokens with the SBOM-push scope checked.
Two optional fields record where the artefact runs and which repository it was built from:
environment: the environment class the artefact runs in, one ofsandbox,development,testorproduction. Anything else is refused withinvalid-environment.source_repo: the repository the artefact was built from, asowner/repo. Anything else is refused withinvalid-source-repo.
-F "environment=production" \
-F "source_repo=acme/payments-api" \Neither field changes retention or any gate. A CycloneDX SBOM with source_repo also lends its dependency graph to that repository’s dependency paths, which is how Maven and Gradle repositories get them.
Where SBOMs surface in the admin UI
Admin → Security › Components › SBOMs lists every ingested SBOM, grouped by artifact. Each row shows:
- Artifact name + version + SHA-256 digest
- Format (CycloneDX / SPDX) + size
- Ingested timestamp + the user/PAT that pushed it
- “Verify signature” link when an attestation is attached
How SBOMs interact with other security signals
- Scorecard — drives the Dependencies + Vulnerabilities scores on the OpenSSF Scorecard computation.
- OSV recommendations — when an OSV advisory matches a component listed in an SBOM, the recommendation surfaces with the SBOM-anchored impact (“this affects build
myapp@v1.2.3from 2026-05-12”). - Cosign — the SBOM-signing flow uses the same Cosign trust root as image signing. See Cosign for the verification path.
Retention
Release SBOMs are kept for as long as your organisation exists. SBOMs of builds that were not released are deleted after your sbom evidence retention: 14 days by default, 0 to 3650 days. The default branch head’s SBOM, current images’ SBOMs and package files’ SBOMs are kept whatever their age. Nothing is deleted while a legal hold is in place. (Until 2026-10-08 this section said SBOMs followed the audit retention presets; they never did.)
See also
- Cosign — image + SBOM signing.
- OSV recommendations — vulnerabilities matched to SBOM components.
- Scorecard — SBOMs drive the dependency + vulnerability scores.
- API reference —
POST /_app/api/v1/orgs/{slug}/sbomsingest endpoint.