Skip to content

Commit 2268b05

Browse files
feat(scanner): Add Nomos plugin for license scanning
- Implement Nomos as an ExternalScanner in ort - add FOSSology Nomos binary to the docker image - add funtest for FOSSology Nomos Signed-off-by: Prakash Mishra <[email protected]>
1 parent bf85ad1 commit 2268b05

File tree

9 files changed

+420
-0
lines changed

9 files changed

+420
-0
lines changed

.env.versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CONAN_VERSION=1.66.0
88
CONAN2_VERSION=2.14.0
99
DART_VERSION=2.18.4
1010
DOTNET_VERSION=6.0
11+
FOSSOLOGY_NOMOSSA_VERSION=4.5.1
1112
GO_VERSION=1.24.5
1213
HASKELL_STACK_VERSION=2.13.1
1314
JAVA_VERSION=21

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,16 @@ COPY --from=ruby --chown=$USER:$USER $RBENV_ROOT $RBENV_ROOT
512512

513513
COPY --from=scancode-license-data --chown=$USER:$USER /opt/scancode-license-data /opt/scancode-license-data
514514

515+
#FOSSology-nomossa
516+
ARG FOSSOLOGY_NOMOSSA_VERSION
517+
518+
RUN mkdir -p /opt/FOSSology-nomossa/bin && \
519+
wget -q https://github.com/fossology/fossology/releases/download/$FOSSOLOGY_NOMOSSA_VERSION/FOSSology-nomossa \
520+
-O /opt/FOSSology-nomossa/bin/FOSSology-nomossa \
521+
&& chmod +x /opt/FOSSology-nomossa/bin/FOSSology-nomossa
522+
523+
ENV PATH=$PATH:/opt/FOSSology-nomossa/bin
524+
515525
#------------------------------------------------------------------------
516526
# Container with all supported package managers.
517527
FROM minimal-tools AS all-tools

NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ Copyright (C) 2023-2024 Double Open Oy
1818
Copyright (C) 2024 Robert Bosch GmbH
1919
Copyright (C) 2024 Cariad SE
2020
Copyright (C) 2025 Quartett mobile GmbH
21+
Copyright (C) 2025 Prakash Mishra
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
plugins {
21+
// Apply precompiled plugins.
22+
id("ort-plugin-conventions")
23+
24+
// Apply third-party plugins.
25+
alias(libs.plugins.kotlinSerialization)
26+
}
27+
28+
dependencies {
29+
api(projects.model)
30+
api(projects.scanner)
31+
32+
implementation(projects.utils.commonUtils)
33+
implementation(projects.utils.ortUtils)
34+
implementation(projects.utils.spdxUtils)
35+
36+
implementation(libs.kotlinx.serialization.json)
37+
38+
ksp(projects.scanner)
39+
40+
funTestApi(testFixtures(projects.scanner))
41+
42+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* SPDX-License-Identifier: Apache-2.0
15+
* License-Filename: LICENSE
16+
*/
17+
18+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
19+
20+
import org.ossreviewtoolkit.model.LicenseFinding
21+
import org.ossreviewtoolkit.model.TextLocation
22+
import org.ossreviewtoolkit.scanner.AbstractPathScannerWrapperFunTest
23+
24+
class FossologyNomossaFunTest : AbstractPathScannerWrapperFunTest() {
25+
override val scanner = NomossaFactory.create()
26+
27+
override val expectedFileLicenses = listOf(
28+
LicenseFinding("Apache-2.0", TextLocation("LICENSE", 1, 195), 100.0f)
29+
)
30+
31+
override val expectedDirectoryLicenses = listOf(
32+
LicenseFinding("Apache-2.0", TextLocation("COPYING", 1, 195), 100.0f),
33+
LicenseFinding("Apache-2.0", TextLocation("LICENCE", 1, 195), 100.0f),
34+
LicenseFinding("Apache-2.0", TextLocation("LICENSE", 1, 195), 100.0f)
35+
)
36+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import java.io.File
23+
import java.time.Instant
24+
25+
import kotlin.math.max
26+
27+
import org.apache.logging.log4j.kotlin.logger
28+
29+
import org.ossreviewtoolkit.model.ScanSummary
30+
import org.ossreviewtoolkit.plugins.api.OrtPlugin
31+
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
32+
import org.ossreviewtoolkit.scanner.LocalPathScannerWrapper
33+
import org.ossreviewtoolkit.scanner.ScanContext
34+
import org.ossreviewtoolkit.scanner.ScannerMatcher
35+
import org.ossreviewtoolkit.scanner.ScannerWrapperFactory
36+
import org.ossreviewtoolkit.utils.common.CommandLineTool
37+
import org.ossreviewtoolkit.utils.common.ProcessCapture
38+
39+
object NomossaCommand : CommandLineTool {
40+
override fun command(workingDir: File?) =
41+
listOfNotNull(workingDir, "FOSSology-nomossa").joinToString(File.separator)
42+
43+
override fun transformVersion(output: String) =
44+
// Example output:
45+
// nomos build version: 4.5.1.1 r(ff4fa7)
46+
output.removePrefix("nomos build version: ").substringBefore(' ') // Returns 4.5.1
47+
48+
override fun getVersionArguments() = "-V"
49+
}
50+
51+
/**
52+
* A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos).
53+
*
54+
* This plugin integrates FOSSology's Nomossa scanner into ORT by calling its CLI
55+
* and mapping its output to ORT's scan result format.
56+
*/
57+
@OrtPlugin(
58+
id = "Nomossa",
59+
displayName = "Nomossa (FOSSology)",
60+
description = "A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos).",
61+
factory = ScannerWrapperFactory::class
62+
)
63+
class Nomossa(
64+
override val descriptor: PluginDescriptor = NomossaFactory.descriptor,
65+
private val config: NomossaConfig
66+
) : LocalPathScannerWrapper() {
67+
private val commandLineOptions by lazy { getCommandLineOptions() }
68+
69+
internal fun getCommandLineOptions() =
70+
buildList {
71+
addAll(config.additionalOptions)
72+
}
73+
74+
override val configuration by lazy {
75+
config.additionalOptions.joinToString(" ")
76+
}
77+
78+
override val matcher by lazy { ScannerMatcher.create(details, config) }
79+
80+
override val version by lazy {
81+
require(NomossaCommand.isInPath()) {
82+
"The '${NomossaCommand.command()}' command is not available in the PATH environment."
83+
}
84+
85+
NomossaCommand.getVersion()
86+
}
87+
88+
override val readFromStorage = config.readFromStorage
89+
override val writeToStorage = config.writeToStorage
90+
91+
override fun runScanner(path: File, context: ScanContext): String {
92+
val process = runNomossa(path)
93+
94+
return with(process) {
95+
if (isError && stdout.isNotBlank()) logger.debug { stdout }
96+
if (stderr.isNotBlank()) logger.debug { stderr }
97+
98+
stdout
99+
}
100+
}
101+
102+
override fun createSummary(result: String, startTime: Instant, endTime: Instant): ScanSummary =
103+
parseNomossaResult(result).toScanSummary(startTime, endTime)
104+
105+
/**
106+
* Execute Nomossa with the configured arguments to scan the given [path].
107+
*/
108+
internal fun runNomossa(path: File): ProcessCapture {
109+
val options = mutableListOf<String>()
110+
options.addAll(commandLineOptions)
111+
112+
if (path.isDirectory) {
113+
options += listOf(
114+
"-n", max(2, Runtime.getRuntime().availableProcessors() - 1).toString(),
115+
"-d", path.absolutePath
116+
)
117+
} else {
118+
options += path.absolutePath
119+
}
120+
121+
return ProcessCapture(
122+
NomossaCommand.command(),
123+
*options.toTypedArray()
124+
)
125+
}
126+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
23+
import org.ossreviewtoolkit.scanner.ScannerMatcherCriteria
24+
25+
/**
26+
* Configuration options for the Nomossa scanner.
27+
*/
28+
data class NomossaConfig(
29+
/**
30+
* Command line options that affect scan results. These are used when matching stored results.
31+
*/
32+
@OrtPluginOption(defaultValue = "-J,-S,-l")
33+
val additionalOptions: List<String>,
34+
35+
/**
36+
* The scanner name pattern used when looking up scan results from storage.
37+
*/
38+
override val regScannerName: String?,
39+
40+
/**
41+
* The minimum version of scan results to use from storage.
42+
*/
43+
override val minVersion: String?,
44+
45+
/**
46+
* The maximum version of scan results to use from storage.
47+
*/
48+
override val maxVersion: String?,
49+
50+
/**
51+
* The configuration string for identifying matching scan results in storage.
52+
*/
53+
override val configuration: String?,
54+
55+
/**
56+
* Whether to read scan results from storage.
57+
*/
58+
@OrtPluginOption(defaultValue = "false")
59+
val readFromStorage: Boolean,
60+
61+
/**
62+
* Whether to write scan results to storage.
63+
*/
64+
@OrtPluginOption(defaultValue = "false")
65+
val writeToStorage: Boolean
66+
) : ScannerMatcherCriteria
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import java.io.File
23+
import java.time.Instant
24+
25+
import org.ossreviewtoolkit.model.LicenseFinding
26+
import org.ossreviewtoolkit.model.ScanSummary
27+
import org.ossreviewtoolkit.model.TextLocation
28+
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
29+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
30+
31+
internal fun NomossaResult.toScanSummary(startTime: Instant, endTime: Instant): ScanSummary {
32+
val licenseFindings = results.flatMap { fileResult ->
33+
val fileContent = File(fileResult.file).readText()
34+
35+
fileResult.licenses.map { licenseInfo ->
36+
val licenseExpression = runCatching { SpdxExpression.parse(licenseInfo.license) }.getOrNull()
37+
38+
val safeLicense = when {
39+
licenseExpression == null -> SpdxConstants.NOASSERTION
40+
licenseExpression.isValid() -> licenseInfo.license
41+
else -> "LicenseRef-Nomossa-${licenseInfo.license.replace(Regex("[^A-Za-z0-9.+-]"), "-")}"
42+
}
43+
val (startLine, endLine) = byteOffsetsToLineNumbers(fileContent, licenseInfo.start, licenseInfo.end)
44+
45+
Triple(fileResult.file, safeLicense, startLine to endLine)
46+
}
47+
}.groupBy { (file, license, _) -> file to license }
48+
.map { (fileAndLicense, entries) ->
49+
val (file, license) = fileAndLicense
50+
val startLine = entries.minOf { it.third.first }
51+
val endLine = entries.maxOf { it.third.second }
52+
53+
LicenseFinding(
54+
license = license,
55+
location = TextLocation(
56+
path = file,
57+
startLine = startLine,
58+
endLine = endLine
59+
),
60+
score = 100.0f
61+
)
62+
}.toSet()
63+
64+
return ScanSummary(
65+
startTime = startTime,
66+
endTime = endTime,
67+
licenseFindings = licenseFindings,
68+
issues = emptyList(),
69+
copyrightFindings = sortedSetOf()
70+
)
71+
}
72+
73+
internal fun byteOffsetsToLineNumbers(fileContent: String, startOffset: Int, endOffset: Int): Pair<Int, Int> {
74+
var startLine = 1
75+
var endLine = 1
76+
77+
for ((index, char) in fileContent.withIndex()) {
78+
if (index >= startOffset && index > endOffset) break
79+
80+
if (char == '\n') {
81+
if (index < startOffset) startLine++
82+
if (index < endOffset) endLine++
83+
}
84+
}
85+
86+
return Pair(startLine, endLine)
87+
}

0 commit comments

Comments
 (0)