Skip to content

Commit f268a13

Browse files
committed
feat(ctrlx-reporter): Allow license filtering based on classifications
The license terms of some proprietary licenses 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]>
1 parent 311ade4 commit f268a13

File tree

4 files changed

+97
-18
lines changed

4 files changed

+97
-18
lines changed

model/src/main/resources/reference.yml

+4
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,10 @@ ort:
382382
user: user
383383
apiKey: XYZ
384384

385+
CtrlXAutomation:
386+
options:
387+
licenseCategoriesToInclude: 'include-in-disclosure-document'
388+
385389
notifier:
386390
mail:
387391
hostName: 'localhost'

model/src/test/kotlin/config/OrtConfigurationTest.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ class OrtConfigurationTest : WordSpec({
372372

373373
with(ortConfig.reporter) {
374374
config shouldNotBeNull {
375-
keys shouldContainExactlyInAnyOrder setOf("CycloneDx", "FossId")
375+
keys shouldContainExactlyInAnyOrder setOf("CycloneDx", "FossId", "CtrlXAutomation")
376376

377377
get("CycloneDx") shouldNotBeNull {
378378
options shouldContainExactly mapOf(
@@ -390,6 +390,13 @@ class OrtConfigurationTest : WordSpec({
390390
"apiKey" to "XYZ"
391391
)
392392
}
393+
394+
get("CtrlXAutomation") shouldNotBeNull {
395+
options shouldContainExactly mapOf(
396+
"licenseCategoriesToInclude" to "include-in-disclosure-document"
397+
)
398+
secrets should beEmpty()
399+
}
393400
}
394401
}
395402

plugins/reporters/ctrlx/src/funTest/kotlin/CtrlXAutomationReporterFunTest.kt

+34-2
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ import org.ossreviewtoolkit.model.RootDependencyIndex
4646
import org.ossreviewtoolkit.model.Scope
4747
import org.ossreviewtoolkit.model.VcsInfo
4848
import org.ossreviewtoolkit.model.VcsType
49+
import org.ossreviewtoolkit.model.licenses.LicenseCategorization
50+
import org.ossreviewtoolkit.model.licenses.LicenseCategory
51+
import org.ossreviewtoolkit.model.licenses.LicenseClassifications
4952
import org.ossreviewtoolkit.plugins.reporters.ctrlx.CtrlXAutomationReporter.Companion.REPORT_FILENAME
5053
import org.ossreviewtoolkit.reporter.ORT_RESULT
5154
import org.ossreviewtoolkit.reporter.ReporterInput
5255
import org.ossreviewtoolkit.utils.ort.createOrtTempDir
56+
import org.ossreviewtoolkit.utils.spdx.SpdxSingleLicenseExpression
5357
import org.ossreviewtoolkit.utils.spdx.toSpdx
5458
import org.ossreviewtoolkit.utils.test.getAssetFile
5559

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

6670
"Generating a report works" {
6771
val outputDir = tempdir()
68-
val reportFiles = CtrlXAutomationReporter().generateReport(ReporterInput(ORT_RESULT), outputDir)
72+
val reportFiles = CtrlXAutomationReporterFactory.create().generateReport(ReporterInput(ORT_RESULT), outputDir)
6973

7074
reportFiles.shouldBeSingleton {
7175
it shouldBeSuccess outputDir.resolve(REPORT_FILENAME)
7276
}
7377
}
7478

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

@@ -87,6 +91,34 @@ class CtrlXAutomationReporterFunTest : StringSpec({
8791
}
8892
}
8993
}
94+
95+
"The reporter should only include licenses with the given category" {
96+
val category = "include-in-disclosure-document"
97+
val categorizations = listOf(
98+
LicenseCategorization(
99+
SpdxSingleLicenseExpression.parse("MIT"),
100+
setOf(category)
101+
)
102+
)
103+
val categories = listOf(LicenseCategory(category))
104+
val input = createReporterInput().copy(
105+
licenseClassifications = LicenseClassifications(
106+
categories = categories,
107+
categorizations = categorizations
108+
)
109+
)
110+
val reporter = CtrlXAutomationReporterFactory.create(listOf(category))
111+
val outputDir = createOrtTempDir("ctrlx-automation-reporter-test")
112+
113+
val reporterResult = reporter.generateReport(input, outputDir)
114+
115+
validateReport(reporterResult) {
116+
components shouldNotBeNull {
117+
this shouldHaveSize 1
118+
first().name shouldBe "package2"
119+
}
120+
}
121+
}
90122
})
91123

92124
private fun validateReport(reporterResult: List<Result<File>>, validate: FossInfo.() -> Unit) {

plugins/reporters/ctrlx/src/main/kotlin/CtrlXAutomationReporter.kt

+51-15
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,35 @@ import kotlinx.serialization.json.encodeToStream
2626

2727
import org.ossreviewtoolkit.model.licenses.LicenseView
2828
import org.ossreviewtoolkit.plugins.api.OrtPlugin
29+
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
2930
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
3031
import org.ossreviewtoolkit.reporter.Reporter
3132
import org.ossreviewtoolkit.reporter.ReporterFactory
3233
import org.ossreviewtoolkit.reporter.ReporterInput
3334
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
3435
import org.ossreviewtoolkit.utils.spdx.SpdxLicense
36+
import org.ossreviewtoolkit.utils.spdx.toSpdx
37+
38+
data class CtrlXAutomationReporterConfig(
39+
/**
40+
* The categories of the licenses of the packages to include in the report. If a component has a license which has a
41+
* category not present in this parameter, the license is removed from the component and not visible in the report.
42+
* If a component has ALL its licenses removed this way, it is not displayed in the report. If the parameter is not
43+
* set for the reporter, all components and all licenses are present in the report.
44+
*/
45+
@OrtPluginOption
46+
val licenseCategoriesToInclude: List<String>?
47+
)
3548

3649
@OrtPlugin(
3750
displayName = "CtrlX Automation Reporter",
3851
description = "A reporter for the ctrlX Automation format.",
3952
factory = ReporterFactory::class
4053
)
41-
class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor) :
54+
class CtrlXAutomationReporter(
55+
override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor,
56+
private val config: CtrlXAutomationReporterConfig
57+
) :
4258
Reporter {
4359
companion object {
4460
const val REPORT_FILENAME = "fossinfo.json"
@@ -54,7 +70,11 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA
5470

5571
override fun generateReport(input: ReporterInput, outputDir: File): List<Result<File>> {
5672
val packages = input.ortResult.getPackages(omitExcluded = true)
57-
val components = packages.mapTo(mutableListOf()) { (pkg, _) ->
73+
val licensesToInclude = config.licenseCategoriesToInclude?.flatMap {
74+
input.licenseClassifications.licensesByCategory[it].orEmpty()
75+
}.orEmpty()
76+
77+
val components = packages.mapNotNullTo(mutableListOf()) { (pkg, _) ->
5878
val qualifiedName = when (pkg.id.type) {
5979
// At least for NPM packages, CtrlX requires the component name to be prefixed with the scope name,
6080
// 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
7393
input.ortResult.getPackageLicenseChoices(pkg.id),
7494
input.ortResult.getRepositoryLicenseChoices()
7595
)
76-
val licenses = effectiveLicense?.decompose()?.map {
96+
var licenses = effectiveLicense?.decompose()?.map {
7797
val name = it.toString()
7898
val spdxId = SpdxLicense.forId(name)?.id
7999
val text = input.licenseTextProvider.getLicenseText(name)
80100
License(name = name, spdx = spdxId, text = text.orEmpty())
81101
}
82102

83-
// The specification requires at least one license.
84-
val componentLicenses = licenses.orEmpty().ifEmpty { listOf(LICENSE_NOASSERTION) }
85-
86-
Component(
87-
name = qualifiedName,
88-
version = pkg.id.version,
89-
homepage = pkg.homepageUrl.takeUnless { it.isEmpty() },
90-
copyright = copyrights?.let { CopyrightInformation(it) },
91-
licenses = componentLicenses,
92-
usage = if (pkg.isModified) Usage.Modified else Usage.AsIs
93-
// TODO: Map the PackageLinkage to an IntegrationMechanism.
94-
)
103+
var componentShouldBeExcluded = false
104+
105+
if (config.licenseCategoriesToInclude != null) {
106+
val filteredLicenses = licenses?.filter { it.name.toSpdx() in licensesToInclude }
107+
108+
if (filteredLicenses != null && filteredLicenses.isEmpty()) {
109+
componentShouldBeExcluded = true
110+
} else {
111+
licenses = filteredLicenses
112+
}
113+
}
114+
115+
if (componentShouldBeExcluded) {
116+
null
117+
} else {
118+
// The specification requires at least one license.
119+
val componentLicenses = licenses.orEmpty().ifEmpty { listOf(LICENSE_NOASSERTION) }
120+
121+
Component(
122+
name = qualifiedName,
123+
version = pkg.id.version,
124+
homepage = pkg.homepageUrl.takeUnless { it.isEmpty() },
125+
copyright = copyrights?.let { CopyrightInformation(it) },
126+
licenses = componentLicenses,
127+
usage = if (pkg.isModified) Usage.Modified else Usage.AsIs
128+
// TODO: Map the PackageLinkage to an IntegrationMechanism.
129+
)
130+
}
95131
}
96132

97133
val reportFileResult = runCatching {

0 commit comments

Comments
 (0)