Software Bill of Materials (SBOM)
Every published llm4s-* module has a CycloneDX SBOM listing the libraries it
depends on. This page says where to find them, what they cover, how to build them yourself, and how the
dependencies are scanned for known vulnerabilities.
Where the SBOMs are
- On each GitHub Release. The
sbomjob of.github/workflows/release.ymlattaches one file per published module, named<module>-<version>.bom.json(for examplellm4s-core-0.5.0.bom.json; the file describes the artifactllm4s-core_3), once the Release exists. It is a separate job, so a failure there turns the run red without touching Maven Central, the Release, the docs deploy or the container image. Attaching it re-uploads over an existing file, so re-running the job is safe. - On every pull request. CI builds and validates the same files and uploads them as the
sbomworkflow artifact for 14 days, so a change that breaks their generation is found before a release.
What they cover
Each BOM lists the runtime dependency tree of one module as resolved by the build: group, name, version, hashes
and a pkg:maven package URL for every library, plus the dependency graph between them. They describe the module’s
compile and runtime classpath, so test frameworks such as ScalaTest are not listed (the exceptions are
llm4s-provider-testkit and llm4s-agent-testkit, whose published APIs are built on ScalaTest, so ScalaTest is a real
dependency of them).
A project that a module depends on only for its tests (dependsOn(other % Test), for example the provider test kit)
is not listed either. The sbt-sbom plugin lists such a project as a required component, although the module’s POM
gives it test scope, so sbt publishedSboms removes the components the described module does not reach through the
BOM’s own dependency graph (project/SbomPrune.scala), and scripts/check-sbom.sh fails if one is left.
A published artifact that holds no code, such as a relocation stub or a bill-of-materials POM that only aligns versions, has no SBOM: it has no dependencies of its own to describe.
The files are CycloneDX 1.6 JSON, the default of the sbt-sbom plugin version the build uses. They contain no
timestamp, serial number or build-machine path, and building the same commit twice gave byte-identical files.
The generation adds nothing to what you depend on: the plugin is a build tool, and the POMs of all published modules are byte-identical with and without it.
Build them locally
1
2
sbt publishedSboms # every published module, copied into target/sboms
sbt core/makeBom # one module, written under that module's target/
Then check them the way CI does:
1
2
sbt -error listPublishedArtifacts | grep -E '^(artifact|stub) ' > target/published-artifacts.txt
scripts/check-sbom.sh target/sboms target/published-artifacts.txt
The check fails for a file that is not CycloneDX JSON, a BOM with no components, a build-machine path, a test
framework in a module that does not ship one, a missing library in the llm4s-core BOM, a published module without
a BOM, a BOM for something that is not published, and a component the described module does not reach through the
dependency graph. scripts/test-check-sbom.sh tests the check itself.
Vulnerability scanning
The Dependency scan workflow (.github/workflows/dependency-scan.yml) builds the SBOMs and runs
OSV-Scanner over them, on pull requests that change build.sbt,
project/Dependencies.scala or project/plugins.sbt, and every Monday. No secret is used, so it runs the same on a
pull request from a fork.
- What fails a pull request. Only a HIGH or CRITICAL advisory (CVSS 7.0 or more, or the advisory database’s
HIGH/CRITICALwhen there is no score) that the pull request’s dependencies have and its base’s do not. The workflow scans the base and the head in the same job, against the same advisory database, so a finding is new only because the change added or moved a dependency. Advisoriesmainalready carries are listed in the job summary but never fail an unrelated change; they are fixed in pull requests of their own. A finding is the pair (advisory, package), so bumping a package from one affected version to another is not new. - Report only on the weekly run, and on a pull request whose base cannot build SBOMs.
- Outages do not block. If OSV-Scanner cannot run on a pull request (the advisory database is unreachable, say), the job warns and passes; the weekly run fails instead, so a broken scan is noticed. A scan that finds no package at all always fails: that is a broken setup, not a clean result.
- Readable output. The job summary has one table: blocking findings first, then other new ones, then those already on the base, each with its advisory link, CVSS score, package, version and the modules that pull it in.
- Why not GitHub’s dependency review. GitHub’s dependency graph does not read sbt builds, so it never sees a Scala dependency change. The SBOMs give OSV-Scanner the exact Maven coordinates instead.
- Two scanner details that otherwise make it scan nothing while reporting only “No package sources found”:
it recognises a CycloneDX file by the name
bom.jsonor*.cdx.json, and it skips files.gitignoreignores (target/is).scripts/osv-scan-sboms.shcopies the SBOMs to*.cdx.jsonoutside the repository and passes--no-ignore. The release keeps the plugin’s*.bom.jsonnames. - Pinned and open source. The scanner binary (OSV-Scanner, Apache-2.0) is a pinned release whose SHA-256 is
checked before it runs. The SBOMs come from the
sbt-sbomplugin (MIT) andcyclonedx-core-java(Apache-2.0), build tools only.
Run the scan yourself, with OSV-Scanner installed:
1
2
3
sbt publishedSboms
OSV_SCANNER=osv-scanner scripts/osv-scan-sboms.sh target/sboms target/osv.json
scripts/osv-new-findings.sh target/osv.json # the report; add a base report as a second argument to diff
scripts/test-osv-scan.sh tests both scripts offline.
What is not covered
- The SBOMs are not signed and there is no build provenance attestation yet.
- They list dependencies, not licences, and say nothing about vulnerabilities by themselves: a finding comes from a scan of the BOM at a point in time.
- The container images (
workspace-runner) have no SBOM.