Skip to content

Commit

Permalink
feat(ctrlx-reporter): Allow license filtering based on classifications
Browse files Browse the repository at this point in the history
Some license terms forbid the license to be disclosed.
This commit adds a new optional parameter to the CtrlX reporter to specify
the categories for which the licenses are included in the report.
If a component has a license which has a category not present in this
parameter, the license is removed from the component and not visible in the
report. If a component has ALL its licenses removed this way, it is not
displayed in the report. If the parameter is not set for the reporter, all
components and all licenses are present in the report.

Signed-off-by: Nicolas Nobelis <[email protected]>
  • Loading branch information
nnobelis committed Jan 29, 2025
1 parent e21a90d commit fec9a68
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 17 deletions.
4 changes: 4 additions & 0 deletions model/src/main/resources/reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ ort:
user: user
apiKey: XYZ

CtrlXAutomation:
options:
licenseCategoriesToInclude: [include-in-disclosure-document]

notifier:
mail:
hostName: 'localhost'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ import org.ossreviewtoolkit.model.RootDependencyIndex
import org.ossreviewtoolkit.model.Scope
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType
import org.ossreviewtoolkit.model.licenses.LicenseCategorization
import org.ossreviewtoolkit.model.licenses.LicenseCategory
import org.ossreviewtoolkit.model.licenses.LicenseClassifications
import org.ossreviewtoolkit.plugins.reporters.ctrlx.CtrlXAutomationReporter.Companion.REPORT_FILENAME
import org.ossreviewtoolkit.reporter.ORT_RESULT
import org.ossreviewtoolkit.reporter.ReporterInput
import org.ossreviewtoolkit.utils.ort.createOrtTempDir
import org.ossreviewtoolkit.utils.spdx.SpdxSingleLicenseExpression
import org.ossreviewtoolkit.utils.spdx.toSpdx
import org.ossreviewtoolkit.utils.test.getAssetFile

Expand All @@ -65,15 +69,15 @@ class CtrlXAutomationReporterFunTest : StringSpec({

"Generating a report works" {
val outputDir = tempdir()
val reportFiles = CtrlXAutomationReporter().generateReport(ReporterInput(ORT_RESULT), outputDir)
val reportFiles = CtrlXAutomationReporterFactory.create().generateReport(ReporterInput(ORT_RESULT), outputDir)

reportFiles.shouldBeSingleton {
it shouldBeSuccess outputDir.resolve(REPORT_FILENAME)
}
}

"Generating a report works and produces a valid fossinfo.json" {
val reporter = CtrlXAutomationReporter()
val reporter = CtrlXAutomationReporterFactory.create()
val input = createReporterInput()
val outputDir = createOrtTempDir("ctrlx-automation-reporter-test")

Expand All @@ -87,6 +91,34 @@ class CtrlXAutomationReporterFunTest : StringSpec({
}
}
}

"The reporter should only include licenses with the given category" {
val category = "include-in-disclosure-document"
val categorizations = listOf(
LicenseCategorization(
SpdxSingleLicenseExpression.parse("MIT"),
setOf(category)
)
)
val categories = listOf(LicenseCategory(category))
val input = createReporterInput().copy(
licenseClassifications = LicenseClassifications(
categories = categories,
categorizations = categorizations
)
)
val reporter = CtrlXAutomationReporterFactory.create(listOf(category))
val outputDir = createOrtTempDir("ctrlx-automation-reporter-test")

val reporterResult = reporter.generateReport(input, outputDir)

validateReport(reporterResult) {
components.shouldNotBeNull {
this shouldHaveSize 1
first().name shouldBe "package2"
}
}
}
})

private fun validateReport(reporterResult: List<Result<File>>, validate: FossInfo.() -> Unit) {
Expand Down
66 changes: 51 additions & 15 deletions plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,35 @@ import kotlinx.serialization.json.encodeToStream

import org.ossreviewtoolkit.model.licenses.LicenseView
import org.ossreviewtoolkit.plugins.api.OrtPlugin
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
import org.ossreviewtoolkit.reporter.Reporter
import org.ossreviewtoolkit.reporter.ReporterFactory
import org.ossreviewtoolkit.reporter.ReporterInput
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
import org.ossreviewtoolkit.utils.spdx.SpdxLicense
import org.ossreviewtoolkit.utils.spdx.toSpdx

data class CtrlXAutomationReporterConfig(
/**
* The categories of the licenses of the packages to include in the report. If a component has a license which has a
* category not present in this parameter, the license is removed from the component and not visible in the report.
* If a component has ALL its licenses removed this way, it is not displayed in the report. If the parameter is not
* set for the reporter, all components and all licenses are present in the report.
*/
@OrtPluginOption
val licenseCategoriesToInclude: List<String>?
)

@OrtPlugin(
displayName = "CtrlX Automation Reporter",
description = "A reporter for the ctrlX Automation format.",
factory = ReporterFactory::class
)
class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor) :
class CtrlXAutomationReporter(
override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor,
private val config: CtrlXAutomationReporterConfig
) :
Reporter {
companion object {
const val REPORT_FILENAME = "fossinfo.json"
Expand All @@ -54,7 +70,11 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA

override fun generateReport(input: ReporterInput, outputDir: File): List<Result<File>> {
val packages = input.ortResult.getPackages(omitExcluded = true)
val components = packages.mapTo(mutableListOf()) { (pkg, _) ->
val licensesToInclude = config.licenseCategoriesToInclude?.flatMap {
input.licenseClassifications.licensesByCategory[it].orEmpty()
}.orEmpty()

val components = packages.mapNotNullTo(mutableListOf()) { (pkg, _) ->
val qualifiedName = when (pkg.id.type) {
// At least for NPM packages, CtrlX requires the component name to be prefixed with the scope name,
// separated with a slash. Other package managers might require similar handling, but there seems to be
Expand All @@ -73,25 +93,41 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA
input.ortResult.getPackageLicenseChoices(pkg.id),
input.ortResult.getRepositoryLicenseChoices()
)
val licenses = effectiveLicense?.decompose()?.map {
var licenses = effectiveLicense?.decompose()?.map {
val name = it.toString()
val spdxId = SpdxLicense.forId(name)?.id
val text = input.licenseTextProvider.getLicenseText(name)
License(name = name, spdx = spdxId, text = text.orEmpty())
}

// The specification requires at least one license.
val componentLicenses = licenses.orEmpty().ifEmpty { listOf(LICENSE_NOASSERTION) }

Component(
name = qualifiedName,
version = pkg.id.version,
homepage = pkg.homepageUrl.takeUnless { it.isEmpty() },
copyright = copyrights?.let { CopyrightInformation(it) },
licenses = componentLicenses,
usage = if (pkg.isModified) Usage.Modified else Usage.AsIs
// TODO: Map the PackageLinkage to an IntegrationMechanism.
)
var componentShouldBeExcluded = false

if (config.licenseCategoriesToInclude != null) {
val filteredLicenses = licenses?.filter { it.name.toSpdx() in licensesToInclude }

if (filteredLicenses != null && filteredLicenses.isEmpty()) {
componentShouldBeExcluded = true
} else {
licenses = filteredLicenses
}
}

if (componentShouldBeExcluded) {
null
} else {
// The specification requires at least one license.
val componentLicenses = licenses.orEmpty().ifEmpty { listOf(LICENSE_NOASSERTION) }

Component(
name = qualifiedName,
version = pkg.id.version,
homepage = pkg.homepageUrl.takeUnless { it.isEmpty() },
copyright = copyrights?.let { CopyrightInformation(it) },
licenses = componentLicenses,
usage = if (pkg.isModified) Usage.Modified else Usage.AsIs
// TODO: Map the PackageLinkage to an IntegrationMechanism.
)
}
}

val reportFileResult = runCatching {
Expand Down

0 comments on commit fec9a68

Please sign in to comment.