From fec9a68014799b6a80f8a295fda46fd8b99d54c0 Mon Sep 17 00:00:00 2001 From: Nicolas Nobelis Date: Mon, 27 Jan 2025 08:29:20 +0100 Subject: [PATCH] feat(ctrlx-reporter): Allow license filtering based on classifications 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 --- model/src/main/resources/reference.yml | 4 ++ .../kotlin/CtrlXAutomationReporterFunTest.kt | 36 +++++++++- .../main/kotlin/CtrlXAutomationReporter.kt | 66 ++++++++++++++----- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/model/src/main/resources/reference.yml b/model/src/main/resources/reference.yml index a036bb4446237..32e9900fc539b 100644 --- a/model/src/main/resources/reference.yml +++ b/model/src/main/resources/reference.yml @@ -382,6 +382,10 @@ ort: user: user apiKey: XYZ + CtrlXAutomation: + options: + licenseCategoriesToInclude: [include-in-disclosure-document] + notifier: mail: hostName: 'localhost' diff --git a/plugins/reporters/ctrlx/src/funTest/kotlin/CtrlXAutomationReporterFunTest.kt b/plugins/reporters/ctrlx/src/funTest/kotlin/CtrlXAutomationReporterFunTest.kt index 2b62aaafc5b76..b29547ab49485 100644 --- a/plugins/reporters/ctrlx/src/funTest/kotlin/CtrlXAutomationReporterFunTest.kt +++ b/plugins/reporters/ctrlx/src/funTest/kotlin/CtrlXAutomationReporterFunTest.kt @@ -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 @@ -65,7 +69,7 @@ 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) @@ -73,7 +77,7 @@ class CtrlXAutomationReporterFunTest : StringSpec({ } "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") @@ -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>, validate: FossInfo.() -> Unit) { diff --git a/plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt b/plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt index 99185b1bbeb9c..a74a501490f07 100644 --- a/plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt +++ b/plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt @@ -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? +) @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" @@ -54,7 +70,11 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA override fun generateReport(input: ReporterInput, outputDir: File): List> { 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 @@ -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 {