Skip to content

Commit abe81f8

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 d38880f commit abe81f8

File tree

4 files changed

+95
-18
lines changed

4 files changed

+95
-18
lines changed

model/src/main/resources/reference.yml

Lines changed: 4 additions & 0 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 34 additions & 2 deletions
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

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,27 @@ import org.ossreviewtoolkit.reporter.ReporterFactory
3232
import org.ossreviewtoolkit.reporter.ReporterInput
3333
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
3434
import org.ossreviewtoolkit.utils.spdx.SpdxLicense
35+
import org.ossreviewtoolkit.utils.spdx.toSpdx
36+
37+
data class CtrlXAutomationReporterConfig(
38+
/**
39+
* The categories of the licenses of the packages to include in the report. If a component has a license which has a
40+
* category not present in this parameter, the license is removed from the component and not visible in the report.
41+
* If a component has ALL its licenses removed this way, it is not displayed in the report. If the parameter is not
42+
* set for the reporter, all components and all licenses are present in the report.
43+
*/
44+
val licenseCategoriesToInclude: List<String>?
45+
)
3546

3647
@OrtPlugin(
3748
displayName = "CtrlX Automation Reporter",
3849
description = "A reporter for the ctrlX Automation format.",
3950
factory = ReporterFactory::class
4051
)
41-
class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor) :
52+
class CtrlXAutomationReporter(
53+
override val descriptor: PluginDescriptor = CtrlXAutomationReporterFactory.descriptor,
54+
private val config: CtrlXAutomationReporterConfig
55+
) :
4256
Reporter {
4357
companion object {
4458
const val REPORT_FILENAME = "fossinfo.json"
@@ -54,7 +68,11 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA
5468

5569
override fun generateReport(input: ReporterInput, outputDir: File): List<Result<File>> {
5670
val packages = input.ortResult.getPackages(omitExcluded = true)
57-
val components = packages.mapTo(mutableListOf()) { (pkg, _) ->
71+
val licensesToInclude = config.licenseCategoriesToInclude?.flatMap {
72+
input.licenseClassifications.licensesByCategory[it].orEmpty()
73+
}.orEmpty()
74+
75+
val components = packages.mapNotNullTo(mutableListOf()) { (pkg, _) ->
5876
val qualifiedName = when (pkg.id.type) {
5977
// At least for NPM packages, CtrlX requires the component name to be prefixed with the scope name,
6078
// separated with a slash. Other package managers might require similar handling, but there seems to be
@@ -73,25 +91,41 @@ class CtrlXAutomationReporter(override val descriptor: PluginDescriptor = CtrlXA
7391
input.ortResult.getPackageLicenseChoices(pkg.id),
7492
input.ortResult.getRepositoryLicenseChoices()
7593
)
76-
val licenses = effectiveLicense?.decompose()?.map {
94+
var licenses = effectiveLicense?.decompose()?.map {
7795
val name = it.toString()
7896
val spdxId = SpdxLicense.forId(name)?.id
7997
val text = input.licenseTextProvider.getLicenseText(name)
8098
License(name = name, spdx = spdxId, text = text.orEmpty())
8199
}
82100

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-
)
101+
var componentShouldBeExcluded = false
102+
103+
if (config.licenseCategoriesToInclude != null) {
104+
val filteredLicenses = licenses?.filter { it.name.toSpdx() in licensesToInclude }
105+
106+
if (filteredLicenses != null && filteredLicenses.isEmpty()) {
107+
componentShouldBeExcluded = true
108+
} else {
109+
licenses = filteredLicenses
110+
}
111+
}
112+
113+
if (componentShouldBeExcluded) {
114+
null
115+
} else {
116+
// The specification requires at least one license.
117+
val componentLicenses = licenses.orEmpty().ifEmpty { listOf(LICENSE_NOASSERTION) }
118+
119+
Component(
120+
name = qualifiedName,
121+
version = pkg.id.version,
122+
homepage = pkg.homepageUrl.takeUnless { it.isEmpty() },
123+
copyright = copyrights?.let { CopyrightInformation(it) },
124+
licenses = componentLicenses,
125+
usage = if (pkg.isModified) Usage.Modified else Usage.AsIs
126+
// TODO: Map the PackageLinkage to an IntegrationMechanism.
127+
)
128+
}
95129
}
96130

97131
val reportFileResult = runCatching {

0 commit comments

Comments
 (0)