SBOM Generation Guide for Scala - sbt

Learn how to generate Software Bill of Materials for Scala projects. Complete guide with build.sbt examples, cross-compilation, and dependency management.

Source vs Build SBOMs

Scala’s primary build tool, sbt (Scala Build Tool), manages dependencies through build.sbt. Unlike some other ecosystems, sbt doesn’t use a traditional lockfile by default, which creates challenges for SBOM generation.

  • Source SBOMs are generated from build.sbt and resolved dependencies
  • Build SBOMs analyze compiled artifacts in the target/ directory

For accurate SBOMs, you need to resolve dependencies first using sbt plugins.

Dependency Declaration

build.sbt

Dependencies are declared in build.sbt:

name := "my-app"
version := "1.0.0"
scalaVersion := "3.3.1"

libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-core" % "2.10.0",
  "io.circe" %% "circe-core" % "0.14.6",
  "io.circe" %% "circe-generic" % "0.14.6",
  "io.circe" %% "circe-parser" % "0.14.6",
  "org.http4s" %% "http4s-ember-server" % "0.23.24",
  "org.http4s" %% "http4s-circe" % "0.23.24",
  "org.http4s" %% "http4s-dsl" % "0.23.24",

  // Test dependencies
  "org.scalatest" %% "scalatest" % "3.2.17" % Test,
  "org.scalamock" %% "scalamock" % "5.2.0" % Test
)

Understanding %% vs %

// %% appends Scala version (cross-compiled)
"org.typelevel" %% "cats-core" % "2.10.0"
// Resolves to: cats-core_3 or cats-core_2.13

// % uses artifact as-is (Java libraries)
"org.postgresql" % "postgresql" % "42.7.0"

Cross-Compilation

Scala libraries are often published for multiple Scala versions:

// project/build.properties
sbt.version=1.9.8

// build.sbt
crossScalaVersions := Seq("2.13.12", "3.3.1")
scalaVersion := "3.3.1"

This means dependencies resolve to different artifacts based on Scala version:

  • cats-core_2.13 for Scala 2.13
  • cats-core_3 for Scala 3

Your SBOM should reflect the specific Scala version used for your build.

sbt-dependency-graph Plugin

For detailed dependency analysis, use the sbt-dependency-graph plugin:

// project/plugins.sbt
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

Generate dependency information:

# ASCII tree
sbt dependencyTree

# Export to file
sbt "dependencyList" > dependencies.txt

# Generate DOT graph
sbt dependencyDot

sbt-dependency-lock Plugin

For lockfile-based reproducibility:

// project/plugins.sbt
addSbtPlugin("io.github.davidgregory084" % "sbt-dependency-lock" % "1.1.0")
# Generate lockfile
sbt dependencyLockWrite

# Verify against lockfile
sbt dependencyLockCheck

This creates build.sbt.lock:

# This file was generated by sbt-dependency-lock
org.scala-lang:scala-library:2.13.12
org.typelevel:cats-core_2.13:2.10.0
org.typelevel:cats-kernel_2.13:2.10.0

Generating an SBOM

SBOM generation is the first step in the SBOM lifecycle. After generation, you typically need to enrich your SBOM with package metadata and augment it with your organization’s details.

The sbomify GitHub Action is a swiss army knife for SBOMs that automatically selects the best generation tool for your ecosystem, enriches the output with package metadata, and optionally augments it with your business information—all in one step.

For Scala, sbomify uses cdxgen under the hood.

Standalone (no account needed):

- uses: sbomify/github-action@master
  env:
    LOCK_FILE: build.sbt
    OUTPUT_FILE: sbom.cdx.json
    COMPONENT_NAME: my-scala-app
    COMPONENT_VERSION: ${{ github.ref_name }}
    ENRICH: true
    UPLOAD: false

Using github.ref_name automatically captures your git tag (e.g., v1.2.3) as the SBOM version. For rolling releases without tags, use github.sha instead. See our SBOM versioning guide for best practices.

With sbomify platform (adds augmentation and upload):

- uses: sbomify/github-action@master
  env:
    TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
    COMPONENT_ID: my-component-id
    LOCK_FILE: build.sbt
    OUTPUT_FILE: sbom.cdx.json
    AUGMENT: true
    ENRICH: true

Alternative Tools

If you prefer to run SBOM generation tools manually:

cdxgen (recommended for manual use):

npm install -g @cyclonedx/cdxgen
cdxgen -t sbt -o sbom.cdx.json

sbt-sbom plugin:

// project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-sbom" % "0.4.0")
sbt sbom

When using these tools directly, you’ll need to handle enrichment and augmentation separately.

GitLab CI

generate-sbom:
  image: sbomifyhub/sbomify-action
  variables:
    LOCK_FILE: build.sbt
    OUTPUT_FILE: sbom.cdx.json
    UPLOAD: "false"
    ENRICH: "true"
  script:
    - /sbomify.sh
  artifacts:
    paths:
      - sbom.cdx.json

Multi-Module Projects

Scala projects often have multiple modules:

// build.sbt
lazy val root = (project in file("."))
  .aggregate(core, api, web)

lazy val core = (project in file("core"))
  .settings(
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.10.0"
    )
  )

lazy val api = (project in file("api"))
  .dependsOn(core)
  .settings(
    libraryDependencies ++= Seq(
      "io.circe" %% "circe-core" % "0.14.6"
    )
  )

lazy val web = (project in file("web"))
  .dependsOn(api)
  .settings(
    libraryDependencies ++= Seq(
      "org.http4s" %% "http4s-ember-server" % "0.23.24"
    )
  )

Generate SBOMs per module or aggregate:

# All modules
sbt dependencyTree

# Specific module
sbt "project api" dependencyTree

Handling Special Cases

Resolvers and Private Repositories

resolvers ++= Seq(
  "Sonatype Releases" at "https://oss.sonatype.org/content/repositories/releases/",
  "Private Repo" at "https://repo.example.com/maven2"
)

credentials += Credentials(Path.userHome / ".sbt" / ".credentials")

Exclusions and Overrides

libraryDependencies ++= Seq(
  "org.example" %% "library" % "1.0.0" exclude("commons-logging", "commons-logging")
)

// Force specific version
dependencyOverrides += "com.google.guava" % "guava" % "32.1.3-jre"

Provided Dependencies

libraryDependencies ++= Seq(
  "javax.servlet" % "javax.servlet-api" % "4.0.1" % Provided
)

Provided dependencies should be documented but noted as provided by the runtime environment.

Compile-time Only

libraryDependencies ++= Seq(
  "org.wartremover" %% "wartremover" % "3.1.6" % "compile->compile"
)

Best Practices

  1. Use dependency locking - Install sbt-dependency-lock for reproducibility
  2. Pin Scala version - Be explicit about scalaVersion
  3. Document cross-compilation - Note which Scala version your SBOM represents
  4. Resolve before generating - Run sbt update before SBOM generation
  5. Separate test dependencies - Use % Test scope appropriately
  6. Check for updates - Use sbt dependencyUpdates with sbt-updates plugin

Security Scanning

Use sbt-dependency-check for vulnerability scanning:

// project/plugins.sbt
addSbtPlugin("net.vonbuchholtz" % "sbt-dependency-check" % "5.1.0")
# Run vulnerability check
sbt dependencyCheck

# Generate report
sbt dependencyCheckReport

Combine with SBOM generation:

- name: Security check
  run: sbt dependencyCheck

- name: Generate SBOM
  uses: sbomify/github-action@master
  env:
    LOCK_FILE: 'build.sbt'
    OUTPUT_FILE: 'sbom.cdx.json'

Mill Build Tool

For projects using Mill instead of sbt:

// build.sc
import mill._
import mill.scalalib._

object myapp extends ScalaModule {
  def scalaVersion = "3.3.1"
  def ivyDeps = Agg(
    ivy"org.typelevel::cats-core:2.10.0",
    ivy"io.circe::circe-core:0.14.6"
  )
}

Mill has different tooling but similar SBOM approaches:

mill show myapp.ivyDepsTree

Further Resources

For more SBOM tools and resources, see our SBOM Resources page, which includes general SBOM utilities for generation, distribution, and analysis.