Skip to content

Commit

Permalink
Merge pull request #49 from siculo/issue/5-BOM-verification
Browse files Browse the repository at this point in the history
Issue/5 bom verification
  • Loading branch information
siculo authored May 13, 2022
2 parents 364c75c + 0654c8b commit 218cc03
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 64 deletions.
3 changes: 3 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/BomError.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.siculo.sbtbom

class BomError(message: String) extends Exception(message)
23 changes: 14 additions & 9 deletions src/main/scala/io/github/siculo/sbtbom/BomExtractor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sbt._
import java.util
import java.util.UUID
import scala.collection.JavaConverters._
import scala.collection.immutable

class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logger) {
private val serialNumber: String = "urn:uuid:" + UUID.randomUUID.toString
Expand Down Expand Up @@ -61,9 +62,9 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg

class ComponentExtractor(moduleReport: ModuleReport) {
def component: Component = {
val group = moduleReport.module.organization
val name = moduleReport.module.name
val version = moduleReport.module.revision
val group: String = moduleReport.module.organization
val name: String = moduleReport.module.name
val version: String = moduleReport.module.revision
/*
moduleReport.extraAttributes found keys are:
- "info.apiURL"
Expand All @@ -72,7 +73,7 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
val component = new Component()
component.setGroup(group)
component.setName(name)
component.setVersion(moduleReport.module.revision)
component.setVersion(version)
component.setModified(false)
component.setType(Component.Type.LIBRARY)
component.setPurl(
Expand All @@ -97,16 +98,20 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
}

private def licenseChoice: Option[LicenseChoice] = {
if (moduleReport.licenses.isEmpty)
val licenses: Seq[model.License] = moduleReport.licenses.map {
case (name, mayBeUrl) =>
model.License(name, mayBeUrl)
}
if (licenses.isEmpty)
None
else {
val choice = new LicenseChoice()
moduleReport.licenses.foreach {
case (name, mayBeUrl) =>
licenses.foreach {
modelLicense =>
val license = new License()
license.setName(name)
license.setName(modelLicense.name)
if (settings.schemaVersion != CycloneDxSchema.Version.VERSION_10) {
mayBeUrl.foreach(license.setUrl)
modelLicense.url.foreach(license.setUrl)
}
choice.addLicense(license)
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/scala/io/github/siculo/sbtbom/BomSbtPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.siculo.sbtbom

import io.github.siculo.sbtbom.PluginConstants._
import org.cyclonedx.model.Component
import sbt.Keys.{artifact, configuration, version}
import sbt.SlashSyntax.RichConfiguration
import sbt.{Def, _}

import scala.language.postfixOps
Expand All @@ -18,6 +18,7 @@ object BomSbtPlugin extends AutoPlugin {

object autoImport {
lazy val bomFileName: SettingKey[String] = settingKey[String]("bom file name")
lazy val bomSchemaVersion: SettingKey[String] = settingKey[String](s"bom schema version; must be one of ${supportedVersionsDescr}; default is ${defaultSupportedVersionDescr}")
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 @@ -36,6 +37,7 @@ object BomSbtPlugin extends AutoPlugin {
}
Seq(
bomFileName := bomFileNameSetting.value,
bomSchemaVersion := defaultSupportedVersion.getVersionString,
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
62 changes: 8 additions & 54 deletions src/main/scala/io/github/siculo/sbtbom/BomSbtSettings.scala
Original file line number Diff line number Diff line change
@@ -1,50 +1,20 @@
package io.github.siculo.sbtbom

import _root_.io.github.siculo.sbtbom.BomSbtPlugin.autoImport._
import org.apache.commons.io.FileUtils
import org.cyclonedx.model.Bom
import org.cyclonedx.{BomGeneratorFactory, CycloneDxSchema}
import sbt.Keys.{configuration, sLog, target}
import sbt.{Compile, Configuration, Def, File, IntegrationTest, Provided, Runtime, Test, _}

import java.nio.charset.Charset
import io.github.siculo.sbtbom.BomSbtPlugin.autoImport._
import sbt.Keys.{sLog, target}
import sbt._

object BomSbtSettings {
val schemaVersion: CycloneDxSchema.Version = CycloneDxSchema.Version.VERSION_10

def makeBomTask(report: UpdateReport, currentConfiguration: Configuration): Def.Initialize[Task[sbt.File]] = Def.task[File] {
val log: Logger = sLog.value

val bomFile = target.value / (currentConfiguration / bomFileName).value

log.info(s"Creating bom file ${bomFile.getAbsolutePath}")

val params = extractorParams(currentConfiguration)
val bom: Bom = new BomExtractor(params, report, log).bom
val bomText: String = getXmlText(bom, schemaVersion)
logBomInfo(log, params, bom)

FileUtils.write(bomFile, bomText, Charset.forName("UTF-8"), false)

log.info(s"Bom file ${bomFile.getAbsolutePath} created")

bomFile
new MakeBomTask(
BomTaskProperties(report, currentConfiguration, sLog.value, bomSchemaVersion.value),
target.value / (currentConfiguration / bomFileName).value
).execute
}

def listBomTask(report: UpdateReport, currentConfiguration: Configuration): Def.Initialize[Task[String]] =
Def.task[String] {
val log: Logger = sLog.value

log.info("Creating bom")

val params = extractorParams(currentConfiguration)
val bom: Bom = new BomExtractor(params, report, log).bom
val bomText: String = getXmlText(bom, schemaVersion)
logBomInfo(log, params, bom)

log.info("Bom created")

bomText
new ListBomTask(BomTaskProperties(report, currentConfiguration, sLog.value, bomSchemaVersion.value)).execute
}

def bomConfigurationTask(currentConfiguration: Option[Configuration]): Def.Initialize[Task[Seq[Configuration]]] =
Expand Down Expand Up @@ -76,20 +46,4 @@ object BomSbtSettings {
}
}

private def extractorParams(currentConfiguration: Configuration): BomExtractorParams =
BomExtractorParams(schemaVersion, currentConfiguration)

private def logBomInfo(log: Logger, params: BomExtractorParams, bom: Bom): Unit = {
log.info(s"Schema version: ${schemaVersion.getVersionString}")
log.info(s"Serial number : ${bom.getSerialNumber}")
log.info(s"Scope : ${params.configuration.id}")
}

private def getXmlText(bom: Bom, schemaVersion: CycloneDxSchema.Version) = {
val bomGenerator = BomGeneratorFactory.createXml(schemaVersion, bom)
bomGenerator.generate
val bomText = bomGenerator.toXmlString
bomText
}

}
81 changes: 81 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/BomTask.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.github.siculo.sbtbom

import io.github.siculo.sbtbom.PluginConstants._
import org.apache.commons.io.FileUtils
import org.cyclonedx.model.Bom
import org.cyclonedx.parsers.XmlParser
import org.cyclonedx.{BomGeneratorFactory, CycloneDxSchema}
import sbt._

import java.nio.charset.Charset
import scala.collection.JavaConverters._

case class BomTaskProperties(report: UpdateReport, currentConfiguration: Configuration, log: Logger, schemaVersion: String)

abstract class BomTask[T](protected val properties: BomTaskProperties) {

def execute: T

protected def getBomText: String = {
val params: BomExtractorParams = extractorParams(currentConfiguration)
val bom: Bom = new BomExtractor(params, report, log).bom
val bomText: String = getXmlText(bom)
logBomInfo(params, bom)
bomText
}

protected def writeToFile(destFile: File, text: String): Unit = {
FileUtils.write(destFile, text, Charset.forName("UTF-8"), false)
}

protected def validateBomFile(bomFile: File): Unit = {
val parser = new XmlParser()
val exceptions = parser.validate(bomFile, schemaVersion).asScala
if (exceptions.nonEmpty) {
val message = s"The BOM file ${bomFile.getAbsolutePath} does not conform to the CycloneDX BOM standard as defined by the XSD"
log.error(s"$message:")
exceptions.foreach {
exception =>
log.error(s"- ${exception.getMessage}")
}
throw new BomError(message)
}
}

@throws[BomError]
protected def raiseException(message: String): Unit = {
log.error(message)
throw new BomError(message)
}

private def extractorParams(currentConfiguration: Configuration): BomExtractorParams =
BomExtractorParams(schemaVersion, currentConfiguration)

private def getXmlText(bom: Bom): String = {
val bomGenerator = BomGeneratorFactory.createXml(schemaVersion, bom)
bomGenerator.generate
val bomText = bomGenerator.toXmlString
bomText
}

protected def logBomInfo(params: BomExtractorParams, bom: Bom): Unit = {
log.info(s"Schema version: ${schemaVersion.getVersionString}")
// log.info(s"Serial number : ${bom.getSerialNumber}")
log.info(s"Scope : ${params.configuration.id}")
}

protected def report: UpdateReport = properties.report

protected def currentConfiguration: Configuration = properties.currentConfiguration

protected def log: Logger = properties.log

protected lazy val schemaVersion: CycloneDxSchema.Version =
supportedVersions.find(_.getVersionString == properties.schemaVersion) match {
case Some(foundVersion) => foundVersion
case None =>
val message = s"Unsupported schema version ${properties.schemaVersion}"
log.error(message)
throw new BomError(message)
}
}
10 changes: 10 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/ListBomTask.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.siculo.sbtbom

class ListBomTask(properties: BomTaskProperties) extends BomTask[String](properties) {
override def execute: String = {
log.info("Creating bom")
val bomText = getBomText
log.info("Bom created")
bomText
}
}
18 changes: 18 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/MakeBomTask.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.siculo.sbtbom

import sbt._

class MakeBomTask(properties: BomTaskProperties,
bomFile: File)
extends BomTask[File](properties) {

override def execute: File = {
log.info(s"Creating bom file ${bomFile.getAbsolutePath}")
val bomText = getBomText
writeToFile(bomFile, bomText)
validateBomFile(bomFile)
log.info(s"Bom file ${bomFile.getAbsolutePath} created")
bomFile
}
}

22 changes: 22 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/PluginConstants.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.siculo.sbtbom

import org.cyclonedx.CycloneDxSchema

object PluginConstants {
val supportedVersions: Seq[CycloneDxSchema.Version] = Seq(
CycloneDxSchema.Version.VERSION_10,
CycloneDxSchema.Version.VERSION_11,
CycloneDxSchema.Version.VERSION_12,
CycloneDxSchema.Version.VERSION_13,
CycloneDxSchema.Version.VERSION_14
)
val defaultSupportedVersion = CycloneDxSchema.Version.VERSION_10
val supportedVersionsDescr: String = {
supportedVersions.take(supportedVersions.size - 1).map(schemaVersionDescr).mkString(", ") + " or " + schemaVersionDescr(supportedVersions.last)
}
val defaultSupportedVersionDescr: String = schemaVersionDescr(defaultSupportedVersion)

private def schemaVersionDescr(version: CycloneDxSchema.Version): String = {
s""""${version.getVersionString}""""
}
}
3 changes: 3 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/model/License.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.siculo.sbtbom.model

case class License(name: String, url: Option[String])
13 changes: 13 additions & 0 deletions src/main/scala/io/github/siculo/sbtbom/model/Module.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.siculo.sbtbom.model

import org.cyclonedx.model.Component.{Scope, Type}

case class Module(
group: String,
name: String,
version: String,
modified: Boolean,
componentType: Type,
componentScope: Scope,
licenses: Seq[License]
)
23 changes: 23 additions & 0 deletions src/sbt-test/schemaVersion/unsupported/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sbt.Keys._

lazy val root = (project in file("."))
.settings(
name := "dependencies",
version := "0.1",
libraryDependencies ++= Dependencies.library,
Test / bomFileName := "bom.xml",
scalaVersion := "2.12.8",
bomSchemaVersion := "999",
check := Def.sequential(
Compile / clean,
Compile / compile,
checkTask
).value
)

lazy val check = taskKey[Unit]("check")
lazy val checkTask = Def.task {
val s: TaskStreams = streams.value
s.log.info("Verifying makeBom param validation...")
(Test / makeBom).value
}
15 changes: 15 additions & 0 deletions src/sbt-test/schemaVersion/unsupported/project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sbt._

object Dependencies {

private val circeVersion = "0.10.0"
private val scalatestVersion = "3.0.5"

lazy val library = Seq(
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.5.2
17 changes: 17 additions & 0 deletions src/sbt-test/schemaVersion/unsupported/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(
sys.props.get("plugin.version"),
sys.props.get("plugin.organization")
) match {
case (Some(version), Some(organization)) =>
addSbtPlugin(organization % "sbt-bom" % version)
case (None, _) =>
sys.error(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin
)
case (_, None) =>
sys.error(
"""|The system property 'plugin.organization' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin
)
}
1 change: 1 addition & 0 deletions src/sbt-test/schemaVersion/unsupported/test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-> check
12 changes: 12 additions & 0 deletions src/test/scala/io/github/siculo/sbtbom/PluginConstantsSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.siculo.sbtbom

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class PluginConstantsSpec extends AnyWordSpec with Matchers {
"PluginConstants" should {
"return the description of the supported versions" in {
PluginConstants.supportedVersionsDescr shouldBe """"1.0", "1.1", "1.2", "1.3" or "1.4""""
}
}
}

0 comments on commit 218cc03

Please sign in to comment.