diff --git a/.env.versions b/.env.versions index 11539b6530412..3b1fc2ee90131 100644 --- a/.env.versions +++ b/.env.versions @@ -8,6 +8,7 @@ CONAN_VERSION=1.66.0 CONAN2_VERSION=2.14.0 DART_VERSION=2.18.4 DOTNET_VERSION=6.0 +FOSSOLOGY_NOMOSSA_VERSION=4.5.1 GO_VERSION=1.24.5 HASKELL_STACK_VERSION=2.13.1 JAVA_VERSION=21 diff --git a/Dockerfile b/Dockerfile index 66e2173c8a3e6..bad148a04cacd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -512,6 +512,16 @@ COPY --from=ruby --chown=$USER:$USER $RBENV_ROOT $RBENV_ROOT COPY --from=scancode-license-data --chown=$USER:$USER /opt/scancode-license-data /opt/scancode-license-data +#FOSSology-nomossa +ARG FOSSOLOGY_NOMOSSA_VERSION + +RUN mkdir -p /opt/FOSSology-nomossa/bin && \ + wget -q https://github.com/fossology/fossology/releases/download/$FOSSOLOGY_NOMOSSA_VERSION/FOSSology-nomossa \ + -O /opt/FOSSology-nomossa/bin/FOSSology-nomossa \ + && chmod +x /opt/FOSSology-nomossa/bin/FOSSology-nomossa + +ENV PATH=$PATH:/opt/FOSSology-nomossa/bin + #------------------------------------------------------------------------ # Container with all supported package managers. FROM minimal-tools AS all-tools diff --git a/NOTICE b/NOTICE index 5f0b724188121..cca26a437ee9b 100644 --- a/NOTICE +++ b/NOTICE @@ -18,3 +18,4 @@ Copyright (C) 2023-2024 Double Open Oy Copyright (C) 2024 Robert Bosch GmbH Copyright (C) 2024 Cariad SE Copyright (C) 2025 Quartett mobile GmbH +Copyright (C) 2025 Prakash Mishra diff --git a/plugins/scanners/fossologynomossa/build.gradle.kts b/plugins/scanners/fossologynomossa/build.gradle.kts new file mode 100644 index 0000000000000..ce454672c0071 --- /dev/null +++ b/plugins/scanners/fossologynomossa/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +plugins { + // Apply precompiled plugins. + id("ort-plugin-conventions") + + // Apply third-party plugins. + alias(libs.plugins.kotlinSerialization) +} + +dependencies { + api(projects.model) + api(projects.scanner) + + implementation(projects.utils.commonUtils) + implementation(projects.utils.ortUtils) + implementation(projects.utils.spdxUtils) + + implementation(libs.kotlinx.serialization.json) + + ksp(projects.scanner) + + funTestApi(testFixtures(projects.scanner)) + +} diff --git a/plugins/scanners/fossologynomossa/src/funTest/kotlin/FossologyNomossaFunTest.kt b/plugins/scanners/fossologynomossa/src/funTest/kotlin/FossologyNomossaFunTest.kt new file mode 100644 index 0000000000000..4f54922b02af4 --- /dev/null +++ b/plugins/scanners/fossologynomossa/src/funTest/kotlin/FossologyNomossaFunTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossologynomossa + +import org.ossreviewtoolkit.model.LicenseFinding +import org.ossreviewtoolkit.model.TextLocation +import org.ossreviewtoolkit.scanner.AbstractPathScannerWrapperFunTest + +class FossologyNomossaFunTest : AbstractPathScannerWrapperFunTest() { + override val scanner = NomossaFactory.create() + + override val expectedFileLicenses = listOf( + LicenseFinding("Apache-2.0", TextLocation("LICENSE", 1, 195), 100.0f) + ) + + override val expectedDirectoryLicenses = listOf( + LicenseFinding("Apache-2.0", TextLocation("COPYING", 1, 195), 100.0f), + LicenseFinding("Apache-2.0", TextLocation("LICENCE", 1, 195), 100.0f), + LicenseFinding("Apache-2.0", TextLocation("LICENSE", 1, 195), 100.0f) + ) +} diff --git a/plugins/scanners/fossologynomossa/src/main/kotlin/Nomossa.kt b/plugins/scanners/fossologynomossa/src/main/kotlin/Nomossa.kt new file mode 100644 index 0000000000000..512d99a21642c --- /dev/null +++ b/plugins/scanners/fossologynomossa/src/main/kotlin/Nomossa.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossologynomossa + +import java.io.File +import java.time.Instant + +import kotlin.math.max + +import org.apache.logging.log4j.kotlin.logger + +import org.ossreviewtoolkit.model.ScanSummary +import org.ossreviewtoolkit.plugins.api.OrtPlugin +import org.ossreviewtoolkit.plugins.api.PluginDescriptor +import org.ossreviewtoolkit.scanner.LocalPathScannerWrapper +import org.ossreviewtoolkit.scanner.ScanContext +import org.ossreviewtoolkit.scanner.ScannerMatcher +import org.ossreviewtoolkit.scanner.ScannerWrapperFactory +import org.ossreviewtoolkit.utils.common.CommandLineTool +import org.ossreviewtoolkit.utils.common.ProcessCapture + +object NomossaCommand : CommandLineTool { + override fun command(workingDir: File?) = + listOfNotNull(workingDir, "FOSSology-nomossa").joinToString(File.separator) + + override fun transformVersion(output: String) = + // Example output: + // nomos build version: 4.5.1.1 r(ff4fa7) + output.removePrefix("nomos build version: ").substringBefore(' ') // Returns 4.5.1 + + override fun getVersionArguments() = "-V" +} + +/** + * A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos). + * + * This plugin integrates FOSSology's Nomossa scanner into ORT by calling its CLI + * and mapping its output to ORT's scan result format. + */ +@OrtPlugin( + id = "Nomossa", + displayName = "Nomossa (FOSSology)", + description = "A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos).", + factory = ScannerWrapperFactory::class +) +class Nomossa( + override val descriptor: PluginDescriptor = NomossaFactory.descriptor, + private val config: NomossaConfig +) : LocalPathScannerWrapper() { + private val commandLineOptions by lazy { getCommandLineOptions() } + + internal fun getCommandLineOptions() = + buildList { + addAll(config.additionalOptions) + } + + override val configuration by lazy { + config.additionalOptions.joinToString(" ") + } + + override val matcher by lazy { ScannerMatcher.create(details, config) } + + override val version by lazy { + require(NomossaCommand.isInPath()) { + "The '${NomossaCommand.command()}' command is not available in the PATH environment." + } + + NomossaCommand.getVersion() + } + + override val readFromStorage = config.readFromStorage + override val writeToStorage = config.writeToStorage + + override fun runScanner(path: File, context: ScanContext): String { + val process = runNomossa(path) + + return with(process) { + if (isError && stdout.isNotBlank()) logger.debug { stdout } + if (stderr.isNotBlank()) logger.debug { stderr } + + stdout + } + } + + override fun createSummary(result: String, startTime: Instant, endTime: Instant): ScanSummary = + parseNomossaResult(result).toScanSummary(startTime, endTime) + + /** + * Execute Nomossa with the configured arguments to scan the given [path]. + */ + internal fun runNomossa(path: File): ProcessCapture { + val options = mutableListOf() + options.addAll(commandLineOptions) + + if (path.isDirectory) { + options += listOf( + "-n", max(2, Runtime.getRuntime().availableProcessors() - 1).toString(), + "-d", path.absolutePath + ) + } else { + options += path.absolutePath + } + + return ProcessCapture( + NomossaCommand.command(), + *options.toTypedArray() + ) + } +} diff --git a/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaConfig.kt b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaConfig.kt new file mode 100644 index 0000000000000..df86205d6ae03 --- /dev/null +++ b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaConfig.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossologynomossa + +import org.ossreviewtoolkit.plugins.api.OrtPluginOption +import org.ossreviewtoolkit.scanner.ScannerMatcherCriteria + +/** + * Configuration options for the Nomossa scanner. + */ +data class NomossaConfig( + /** + * Command line options that affect scan results. These are used when matching stored results. + */ + @OrtPluginOption(defaultValue = "-J,-S,-l") + val additionalOptions: List, + + /** + * The scanner name pattern used when looking up scan results from storage. + */ + override val regScannerName: String?, + + /** + * The minimum version of scan results to use from storage. + */ + override val minVersion: String?, + + /** + * The maximum version of scan results to use from storage. + */ + override val maxVersion: String?, + + /** + * The configuration string for identifying matching scan results in storage. + */ + override val configuration: String?, + + /** + * Whether to read scan results from storage. + */ + @OrtPluginOption(defaultValue = "false") + val readFromStorage: Boolean, + + /** + * Whether to write scan results to storage. + */ + @OrtPluginOption(defaultValue = "false") + val writeToStorage: Boolean +) : ScannerMatcherCriteria diff --git a/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultExtensions.kt b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultExtensions.kt new file mode 100644 index 0000000000000..9f3b77f6104ca --- /dev/null +++ b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultExtensions.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossologynomossa + +import java.io.File +import java.time.Instant + +import org.ossreviewtoolkit.model.LicenseFinding +import org.ossreviewtoolkit.model.ScanSummary +import org.ossreviewtoolkit.model.TextLocation +import org.ossreviewtoolkit.utils.spdx.SpdxConstants +import org.ossreviewtoolkit.utils.spdx.SpdxExpression + +internal fun NomossaResult.toScanSummary(startTime: Instant, endTime: Instant): ScanSummary { + val licenseFindings = results.flatMap { fileResult -> + val fileContent = File(fileResult.file).readText() + + fileResult.licenses.map { licenseInfo -> + val licenseExpression = runCatching { SpdxExpression.parse(licenseInfo.license) }.getOrNull() + + val safeLicense = when { + licenseExpression == null -> SpdxConstants.NOASSERTION + licenseExpression.isValid() -> licenseInfo.license + else -> "LicenseRef-Nomossa-${licenseInfo.license.replace(Regex("[^A-Za-z0-9.+-]"), "-")}" + } + val (startLine, endLine) = byteOffsetsToLineNumbers(fileContent, licenseInfo.start, licenseInfo.end) + + Triple(fileResult.file, safeLicense, startLine to endLine) + } + }.groupBy { (file, license, _) -> file to license } + .map { (fileAndLicense, entries) -> + val (file, license) = fileAndLicense + val startLine = entries.minOf { it.third.first } + val endLine = entries.maxOf { it.third.second } + + LicenseFinding( + license = license, + location = TextLocation( + path = file, + startLine = startLine, + endLine = endLine + ), + score = 100.0f + ) + }.toSet() + + return ScanSummary( + startTime = startTime, + endTime = endTime, + licenseFindings = licenseFindings, + issues = emptyList(), + copyrightFindings = sortedSetOf() + ) +} + +internal fun byteOffsetsToLineNumbers(fileContent: String, startOffset: Int, endOffset: Int): Pair { + var startLine = 1 + var endLine = 1 + + for ((index, char) in fileContent.withIndex()) { + if (index >= startOffset && index > endOffset) break + + if (char == '\n') { + if (index < startOffset) startLine++ + if (index < endOffset) endLine++ + } + } + + return Pair(startLine, endLine) +} diff --git a/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultParser.kt b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultParser.kt new file mode 100644 index 0000000000000..c8e0c476e44f8 --- /dev/null +++ b/plugins/scanners/fossologynomossa/src/main/kotlin/NomossaResultParser.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossologynomossa + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +internal data class NomossaResult( + val results: List +) + +@Serializable +internal data class NomossaFileResult( + val file: String, + val licenses: List +) + +@Serializable +internal data class NomossaLicenseInfo( + val license: String, + val start: Int, + val end: Int, + val len: Int +) + +private val json = Json { + ignoreUnknownKeys = true +} + +/** + * Parses the JSON result string returned by Nomossa into a [NomossaResult] object. + */ +internal fun parseNomossaResult(result: String): NomossaResult = json.decodeFromString(result)