Skip to content

Commit

Permalink
SbomExtractor improvements (sbt#95)
Browse files Browse the repository at this point in the history
* SbomExtractor improvements

* use LicensesArchive to find license id

* make bom hashes configurable

* settings for bom attributes

* disable sha3 hashes in test

* mention enableBomSha3Hashes in README
  • Loading branch information
lhns authored Dec 12, 2024
1 parent 4efd003 commit 062e821
Show file tree
Hide file tree
Showing 14 changed files with 593 additions and 710 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ The `listBom` command can be used to generate the contents of the BOM without wr

### configuration

| Setting | Type | Description |
|-------------|--------|---------------|
| bomFileName | String | bom file name |
| Setting | Type | Default | Description |
|------------------------|---------|----------------------------------------------|-----------------------------------------------------------------|
| bomFileName | String | `"${artifactId}-${artifactVersion}.bom.xml"` | bom file name |
| bomSchemaVersion | String | `"1.6"` | bom schema version |
| includeBomSerialNumber | Boolean | `false` | include serial number in bom |
| includeBomTimestamp | Boolean | `false` | include timestamp in bom |
| includeBomToolVersion | Boolean | `true` | include tool version in bom |
| includeBomHashes | Boolean | `true` | include artifact hashes in bom |
| enableBomSha3Hashes | Boolean | `true` | enable the generation of sha3 hashes (not available on java 8) |

Sample configuration:

Expand Down
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ ThisBuild / scmInfo := Project.scmInfo
ThisBuild / description := Project.description

lazy val root = (project in file("."))
.enablePlugins(ScriptedPlugin)
.enablePlugins(ScriptedPlugin, BuildInfoPlugin)
.settings(
name := "sbt-sbom",
sbtPlugin := true,
libraryDependencies ++= Dependencies.library,
buildInfoPackage := "com.github.sbt.sbom",
scriptedLaunchOpts := {
scriptedLaunchOpts.value ++ Seq(
"-Xmx1024M",
Expand Down
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0")
addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.24.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
123 changes: 98 additions & 25 deletions src/main/scala/com/github/sbt/sbom/BomExtractor.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.github.sbt.sbom

import com.github.packageurl.PackageURL
import com.github.sbt.sbom.licenses.LicensesArchive
import org.cyclonedx.Version
import org.cyclonedx.model.{ Bom, Component, License, LicenseChoice }
import org.cyclonedx.model.{ Bom, Component, Hash, License, LicenseChoice, Metadata, Tool }
import org.cyclonedx.util.BomUtils
import sbt._
import sbt.librarymanagement.ModuleReport

Expand All @@ -18,14 +20,45 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
if (settings.includeBomSerialNumber && settings.schemaVersion != Version.VERSION_10) {
bom.setSerialNumber(serialNumber)
}
if (settings.schemaVersion.getVersion >= Version.VERSION_12.getVersion) {
bom.setMetadata(metadata)
}
bom.setComponents(components.asJava)
bom
}

private def components: Seq[Component] =
configurationsForComponents(settings.configuration).foldLeft(Seq[Component]()) { case (collected, configuration) =>
collected ++ componentsForConfiguration(configuration)
private lazy val metadata: Metadata = {
val metadata = new Metadata()
if (!settings.includeBomTimestamp) {
metadata.setTimestamp(null)
}
metadata.addTool(tool)
metadata
}

private lazy val tool: Tool = {
val tool = new Tool()
// https://github.com/devops-kung-fu/bomber/blob/main/lib/loader.go#L112 searches for string CycloneDX to detect format
tool.setName("CycloneDX SBT plugin")
if (settings.includeBomToolVersion) {
tool.setVersion(BuildInfo.version)
}
tool
}

private def components: Seq[Component] = {
val components = configurationsForComponents(settings.configuration).flatMap { configuration =>
componentsForConfiguration(configuration)
}.distinct // deduplicate components reported by multiple configurations
components.groupBy(_.getBomRef).foreach {
case (null, _) => () // ignore empty bom-refs
case (_, Seq(_)) => () // no duplicate bom-refs
case (bomRef, components) => // duplicate bom-refs
log.warn(s"bom-ref must be distinct: $bomRef")
components.foreach(_.setBomRef(null))
}
components
}

private def configurationsForComponents(configuration: Configuration): Seq[sbt.Configuration] = {
log.info(s"Current configuration = ${configuration.name}")
Expand All @@ -48,14 +81,17 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
}

private def componentsForConfiguration(configuration: Configuration): Seq[Component] = {
(report.configuration(configuration) map { configurationReport =>
log.info(
s"Configuration name = ${configurationReport.configuration.name}, modules: ${configurationReport.modules.size}"
)
configurationReport.modules.map { module =>
new ComponentExtractor(module).component
report
.configuration(configuration)
.map { configurationReport =>
log.info(
s"Configuration name = ${configurationReport.configuration.name}, modules: ${configurationReport.modules.size}"
)
configurationReport.modules.map { module =>
new ComponentExtractor(module).component
}
}
}).getOrElse(Seq())
.getOrElse(Seq())
}

class ComponentExtractor(moduleReport: ModuleReport) {
Expand All @@ -77,13 +113,19 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
component.setPurl(
new PackageURL(PackageURL.StandardTypes.MAVEN, group, name, version, new util.TreeMap(), null).canonicalize()
)
if (settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
// component bom-refs must be unique
component.setBomRef(component.getPurl)
}
component.setScope(Component.Scope.REQUIRED)
licenseChoice.foreach(component.setLicenseChoice)
if (settings.includeBomHashes) {
component.setHashes(hashes(artifactPaths(moduleReport)).asJava)
}
licenseChoice.foreach(component.setLicenses)

/*
not returned component properties are (BOM version 1.0):
- publisher: The person(s) or organization(s) that published the component
- hashes
- copyright: An optional copyright notice informing users of the underlying claims to copyright ownership in a published work.
- cpe: Specifies a well-formed CPE name. See https://nvd.nist.gov/products/cpe
- components: Specifies optional sub-components. This is not a dependency tree. It simply provides an optional way to group large sets of components together.
Expand All @@ -95,22 +137,53 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
component
}

private def artifactPaths(moduleReport: ModuleReport): Seq[File] =
moduleReport.artifacts
.map { case (_, file) =>
file
}
.filter { file =>
file.exists() && file.isFile
}

private def hashes(files: Seq[File]): Seq[Hash] =
files.flatMap { file =>
val hashes = BomUtils.calculateHashes(file, settings.schemaVersion).asScala
if (settings.enableBomSha3Hashes) {
hashes
} else {
hashes.filterNot(_.getAlgorithm.matches("(?i)SHA3-.*"))
}
}

private def licenseChoice: Option[LicenseChoice] = {
val licenses: Seq[model.License] = moduleReport.licenses.map { case (name, mayBeUrl) =>
model.License(name, mayBeUrl)
val licensesArchive = LicensesArchive.bundled
val licenses: Seq[License] = moduleReport.licenses.map { case (name, mayBeUrl) =>
val license = new License()
licensesArchive
.findById(name)
.orElse(mayBeUrl.map(licensesArchive.findByUrl).collect { case Seq(license) =>
license
})
.foreach { archiveLicense =>
license.setId(archiveLicense.id)
}
if (license.getId == null) {
// must not be set if id is defined
license.setName(name)
}
mayBeUrl.foreach { url =>
if (settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
license.setUrl(url)
}
}
license
}
if (licenses.isEmpty)
if (licenses.isEmpty) {
None
else {
} else {
val choice = new LicenseChoice()
licenses.foreach { modelLicense =>
val license = new License()
license.setName(modelLicense.name)
if (settings.schemaVersion != Version.VERSION_10) {
modelLicense.url.foreach(license.setUrl)
}
choice.addLicense(license)
}
licenses.foreach(choice.addLicense)
Some(choice)
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/com/github/sbt/sbom/BomExtractorParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ final case class BomExtractorParams(
schemaVersion: Version,
configuration: Configuration,
includeBomSerialNumber: Boolean,
includeBomTimestamp: Boolean,
includeBomToolVersion: Boolean,
includeBomHashes: Boolean,
enableBomSha3Hashes: Boolean,
)
16 changes: 16 additions & 0 deletions src/main/scala/com/github/sbt/sbom/BomSbtPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ object BomSbtPlugin extends AutoPlugin {
lazy val includeBomSerialNumber: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain a serial number? default is false, because the current mechanism for determining the serial number is not reproducible"
)
lazy val includeBomTimestamp: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain a timestamp? default is false, because the timestamp is not reproducible"
)
lazy val includeBomToolVersion: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain the tool version? default is true"
)
lazy val includeBomHashes: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain artifact hashes? default is true"
)
lazy val enableBomSha3Hashes: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM artifact hashes contain sha3 hashes? default is true"
)
lazy val makeBom: TaskKey[sbt.File] = taskKey[sbt.File]("Generates bom file")
lazy val listBom: TaskKey[String] = taskKey[String]("Returns the bom")
lazy val components: TaskKey[Component] = taskKey[Component]("Returns the bom")
Expand All @@ -46,6 +58,10 @@ object BomSbtPlugin extends AutoPlugin {
bomFileName := bomFileNameSetting.value,
bomSchemaVersion := defaultSupportedVersion.getVersionString,
includeBomSerialNumber := false,
includeBomTimestamp := false,
includeBomToolVersion := true,
includeBomHashes := true,
enableBomSha3Hashes := true,
makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Compile)).value,
listBom := Def.taskDyn(BomSbtSettings.listBomTask(Classpaths.updateTask.value, Compile)).value,
Test / makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Test)).value,
Expand Down
12 changes: 10 additions & 2 deletions src/main/scala/com/github/sbt/sbom/BomSbtSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ object BomSbtSettings {
currentConfiguration,
sLog.value,
bomSchemaVersion.value,
includeBomSerialNumber.value
includeBomSerialNumber.value,
includeBomTimestamp.value,
includeBomToolVersion.value,
includeBomHashes.value,
enableBomSha3Hashes.value
),
target.value / (currentConfiguration / bomFileName).value
).execute
Expand All @@ -27,7 +31,11 @@ object BomSbtSettings {
currentConfiguration,
sLog.value,
bomSchemaVersion.value,
includeBomSerialNumber.value
includeBomSerialNumber.value,
includeBomTimestamp.value,
includeBomToolVersion.value,
includeBomHashes.value,
enableBomSha3Hashes.value
)
).execute
}
Expand Down
22 changes: 21 additions & 1 deletion src/main/scala/com/github/sbt/sbom/BomTask.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ final case class BomTaskProperties(
log: Logger,
schemaVersion: String,
includeBomSerialNumber: Boolean,
includeBomTimestamp: Boolean,
includeBomToolVersion: Boolean,
includeBomHashes: Boolean,
enableBomSha3Hashes: Boolean,
)

abstract class BomTask[T](protected val properties: BomTaskProperties) {
Expand Down Expand Up @@ -56,7 +60,15 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
}

private def extractorParams(currentConfiguration: Configuration): BomExtractorParams =
BomExtractorParams(schemaVersion, currentConfiguration, includeBomSerialNumber)
BomExtractorParams(
schemaVersion,
currentConfiguration,
includeBomSerialNumber,
includeBomTimestamp,
includeBomToolVersion,
includeBomHashes,
enableBomSha3Hashes
)

private def getXmlText(bom: Bom): String = {
val bomGenerator = BomGeneratorFactory.createXml(schemaVersion, bom)
Expand Down Expand Up @@ -87,4 +99,12 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
}

protected lazy val includeBomSerialNumber: Boolean = properties.includeBomSerialNumber

protected lazy val includeBomTimestamp: Boolean = properties.includeBomTimestamp

protected lazy val includeBomToolVersion: Boolean = properties.includeBomToolVersion

protected lazy val includeBomHashes: Boolean = properties.includeBomHashes

protected lazy val enableBomSha3Hashes: Boolean = properties.enableBomSha3Hashes
}
2 changes: 2 additions & 0 deletions src/sbt-test/dependencies/compile/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ lazy val root = (project in file("."))
version := "0.1",
libraryDependencies ++= Dependencies.library,
bomFileName := "bom.xml",
includeBomToolVersion := false,
enableBomSha3Hashes := false,
scalaVersion := "2.12.20",
check := Def
.sequential(
Expand Down
Loading

0 comments on commit 062e821

Please sign in to comment.